home *** CD-ROM | disk | FTP | other *** search
/ Clickx 47 / Clickx 47.iso / assets / software / Miro_Installer.exe / xulrunner / python / feed.py < prev    next >
Encoding:
Python Source  |  2008-01-10  |  97.1 KB  |  2,678 lines

  1. # Miro - an RSS based video player application
  2. # Copyright (C) 2005-2007 Participatory Culture Foundation
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  17.  
  18. # FIXME import * is really bad practice..  At the very least, lest keep it at
  19. # the top, so it cant overwrite other symbols.
  20. from item import *
  21.  
  22. from HTMLParser import HTMLParser,HTMLParseError
  23. from cStringIO import StringIO
  24. from copy import copy
  25. from datetime import datetime, timedelta
  26. from gtcache import gettext as _
  27. from inspect import isfunction
  28. from new import instancemethod
  29. from urlparse import urlparse, urljoin
  30. from xhtmltools import unescape,xhtmlify,fixXMLHeader, fixHTMLHeader, urlencode, urldecode
  31. import os
  32. import string
  33. import re
  34. import traceback
  35. import xml
  36.  
  37. from database import defaultDatabase, DatabaseConstraintError
  38. from httpclient import grabURL, NetworkError
  39. from iconcache import iconCacheUpdater, IconCache
  40. from templatehelper import quoteattr, escape, toUni
  41. from string import Template
  42. import app
  43. import config
  44. import dialogs
  45. import eventloop
  46. import folder
  47. import menu
  48. import prefs
  49. import resources
  50. import downloader
  51. from util import returnsUnicode, unicodify, chatter, checkU, checkF, quoteUnicodeURL
  52. from fileutil import miro_listdir
  53. from platformutils import filenameToUnicode, makeURLSafe, unmakeURLSafe, osFilenameToFilenameType, FilenameType
  54. import filetypes
  55. import views
  56. import indexes
  57. import searchengines
  58. import sorts
  59. import logging
  60. import shutil
  61. from clock import clock
  62.  
  63. whitespacePattern = re.compile(r"^[ \t\r\n]*$")
  64.  
  65. @returnsUnicode
  66. def defaultFeedIconURL():
  67.     return resources.url(u"images/feedicon.png")
  68.  
  69. @returnsUnicode
  70. def defaultFeedIconURLTablist():
  71.     return resources.url(u"images/feedicon-tablist.png")
  72.  
  73. # Notes on character set encoding of feeds:
  74. #
  75. # The parsing libraries built into Python mostly use byte strings
  76. # instead of unicode strings.  However, sometimes they get "smart" and
  77. # try to convert the byte stream to a unicode stream automatically.
  78. #
  79. # What does what when isn't clearly documented
  80. #
  81. # We use the function toUni() to fix those smart conversions
  82. #
  83. # If you run into Unicode crashes, adding that function in the
  84. # appropriate place should fix it.
  85.  
  86. # Universal Feed Parser http://feedparser.org/
  87. # Licensed under Python license
  88. import feedparser
  89.  
  90. # Pass in a connection to the frontend
  91. def setDelegate(newDelegate):
  92.     global delegate
  93.     delegate = newDelegate
  94.  
  95. # Pass in a feed sorting function 
  96. def setSortFunc(newFunc):
  97.     global sortFunc
  98.     sortFunc = newFunc
  99.  
  100. #
  101. # Adds a new feed using USM
  102. def addFeedFromFile(file):
  103.     checkF(file)
  104.     d = feedparser.parse(file)
  105.     if d.feed.has_key('links'):
  106.         for link in d.feed['links']:
  107.             if link['rel'] == 'start' or link['rel'] == 'self':
  108.                 Feed(link['href'])
  109.                 return
  110.     if d.feed.has_key('link'):
  111.         addFeedFromWebPage(d.feed.link)
  112.  
  113. #
  114. # Adds a new feed based on a link tag in a web page
  115. def addFeedFromWebPage(url):
  116.     checkU(url)
  117.     def callback(info):
  118.         url = HTMLFeedURLParser().getLink(info['updated-url'],info['body'])
  119.         if url:
  120.             Feed(url)
  121.     def errback(error):
  122.         logging.warning ("unhandled error in addFeedFromWebPage: %s", error)
  123.     grabURL(url, callback, errback)
  124.  
  125. # URL validitation and normalization
  126. def validateFeedURL(url):
  127.     checkU(url)
  128.     for c in url.encode('utf8'):
  129.         if ord(c) > 127:
  130.             return False
  131.     if re.match(r"^(http|https)://[^/ ]+/[^ ]*$", url) is not None:
  132.         return True
  133.     if re.match(r"^file://.", url) is not None:
  134.         return True
  135.     match = re.match(r"^dtv:searchTerm:(.*)\?(.*)$", url)
  136.     if match is not None and validateFeedURL(urldecode(match.group(1))):
  137.         return True
  138.     match = re.match(r"^dtv:multi:", url)
  139.     if match is not None:
  140.         return True
  141.     return False
  142.  
  143. def normalizeFeedURL(url):
  144.     checkU(url)
  145.     # Valid URL are returned as-is
  146.     if validateFeedURL(url):
  147.         return url
  148.  
  149.     searchTerm = None
  150.     m = re.match(r"^dtv:searchTerm:(.*)\?([^?]+)$", url)
  151.     if m is not None:
  152.         searchTerm = urldecode(m.group(2))
  153.         url = urldecode(m.group(1))
  154.  
  155.     originalURL = url
  156.     url = url.strip()
  157.     
  158.     # Check valid schemes with invalid separator
  159.     match = re.match(r"^(http|https):/*(.*)$", url)
  160.     if match is not None:
  161.         url = "%s://%s" % match.group(1,2)
  162.  
  163.     # Replace invalid schemes by http
  164.     match = re.match(r"^(([A-Za-z]*):/*)*(.*)$", url)
  165.     if match and match.group(2) in ['feed', 'podcast', 'fireant', None]:
  166.         url = "http://%s" % match.group(3)
  167.     elif match and match.group(1) == 'feeds':
  168.         url = "https://%s" % match.group(3)
  169.  
  170.     # Make sure there is a leading / character in the path
  171.     match = re.match(r"^(http|https)://[^/]*$", url)
  172.     if match is not None:
  173.         url = url + "/"
  174.  
  175.     if searchTerm is not None:
  176.         url = "dtv:searchTerm:%s?%s" % (urlencode(url), urlencode(searchTerm))
  177.     else:
  178.         url = quoteUnicodeURL(url)
  179.  
  180.     if not validateFeedURL(url):
  181.         logging.info ("unable to normalize URL %s", originalURL)
  182.         return originalURL
  183.     else:
  184.         return url
  185.  
  186.  
  187. ##
  188. # Handle configuration changes so we can update feed update frequencies
  189.  
  190. def configDidChange(key, value):
  191.     if key is prefs.CHECK_CHANNELS_EVERY_X_MN.key:
  192.         for feed in views.feeds:
  193.             updateFreq = 0
  194.             try:
  195.                 updateFreq = feed.parsed["feed"]["ttl"]
  196.             except:
  197.                 pass
  198.             feed.setUpdateFrequency(updateFreq)
  199.  
  200. config.addChangeCallback(configDidChange)
  201.  
  202. ##
  203. # Actual implementation of a basic feed.
  204. class FeedImpl:
  205.     def __init__(self, url, ufeed, title = None, visible = True):
  206.         checkU(url)
  207.         if title:
  208.             checkU(title)
  209.         self.url = url
  210.         self.ufeed = ufeed
  211.         self.calc_item_list()
  212.         if title == None:
  213.             self.title = url
  214.         else:
  215.             self.title = title
  216.         self.created = datetime.now()
  217.         self.visible = visible
  218.         self.updating = False
  219.         self.lastViewed = datetime.min
  220.         self.thumbURL = defaultFeedIconURL()
  221.         self.initialUpdate = True
  222.         self.updateFreq = config.get(prefs.CHECK_CHANNELS_EVERY_X_MN)*60
  223.  
  224.     def calc_item_list(self):
  225.         self.items = views.toplevelItems.filterWithIndex(indexes.itemsByFeed, self.ufeed.id)
  226.         self.availableItems = self.items.filter(lambda x: x.getState() == 'new')
  227.         self.unwatchedItems = self.items.filter(lambda x: x.getState() == 'newly-downloaded')
  228.         self.availableItems.addAddCallback(lambda x,y:self.ufeed.signalChange(needsSignalFolder = True))
  229.         self.availableItems.addRemoveCallback(lambda x,y:self.ufeed.signalChange(needsSignalFolder = True))
  230.         self.unwatchedItems.addAddCallback(lambda x,y:self.ufeed.signalChange(needsSignalFolder = True))
  231.         self.unwatchedItems.addRemoveCallback(lambda x,y:self.ufeed.signalChange(needsSignalFolder = True))
  232.         
  233.     def signalChange(self):
  234.         self.ufeed.signalChange()
  235.  
  236.     @returnsUnicode
  237.     def getBaseHref(self):
  238.         """Get a URL to use in the <base> tag for this channel.  This is used
  239.         for relative links in this channel's items.
  240.         """
  241.         return escape(self.url)
  242.  
  243.     # Sets the update frequency (in minutes). 
  244.     # - A frequency of -1 means that auto-update is disabled.
  245.     def setUpdateFrequency(self, frequency):
  246.         try:
  247.             frequency = int(frequency)
  248.         except ValueError:
  249.             frequency = -1
  250.  
  251.         if frequency < 0:
  252.             self.cancelUpdateEvents()
  253.             self.updateFreq = -1
  254.         else:
  255.             newFreq = max(config.get(prefs.CHECK_CHANNELS_EVERY_X_MN),
  256.                           frequency)*60
  257.             if newFreq != self.updateFreq:
  258.                 self.updateFreq = newFreq
  259.                 self.scheduleUpdateEvents(-1)
  260.         self.ufeed.signalChange()
  261.  
  262.     def scheduleUpdateEvents(self, firstTriggerDelay):
  263.         self.cancelUpdateEvents()
  264.         if firstTriggerDelay >= 0:
  265.             self.scheduler = eventloop.addTimeout(firstTriggerDelay, self.update, "Feed update (%s)" % self.getTitle())
  266.         else:
  267.             if self.updateFreq > 0:
  268.                 self.scheduler = eventloop.addTimeout(self.updateFreq, self.update, "Feed update (%s)" % self.getTitle())
  269.  
  270.     def cancelUpdateEvents(self):
  271.         if hasattr(self, 'scheduler') and self.scheduler is not None:
  272.             self.scheduler.cancel()
  273.             self.scheduler = None
  274.  
  275.     # Subclasses should override this
  276.     def update(self):
  277.         self.scheduleUpdateEvents(-1)
  278.  
  279.     # Returns true iff this feed has been looked at
  280.     def getViewed(self):
  281.         return self.lastViewed != datetime.min
  282.  
  283.     # Returns the ID of the actual feed, never that of the UniversalFeed wrapper
  284.     def getFeedID(self):
  285.         return self.getID()
  286.  
  287.     def getID(self):
  288.         try:
  289.             return self.ufeed.getID()
  290.         except:
  291.             logging.info ("%s has no ufeed", self)
  292.  
  293.     # Returns string with number of unwatched videos in feed
  294.     def numUnwatched(self):
  295.         return len(self.unwatchedItems)
  296.  
  297.     # Returns string with number of available videos in feed
  298.     def numAvailable(self):
  299.         return len(self.availableItems)
  300.  
  301.     # Returns true iff both unwatched and available numbers should be shown
  302.     def showBothUAndA(self):
  303.         return self.showU() and self.showA()
  304.  
  305.     # Returns true iff unwatched should be shown 
  306.     def showU(self):
  307.         return len(self.unwatchedItems) > 0
  308.  
  309.     # Returns true iff available should be shown
  310.     def showA(self):
  311.         return len(self.availableItems) > 0 and not self.isAutoDownloadable()
  312.  
  313.     ##
  314.     # Sets the last time the feed was viewed to now
  315.     def markAsViewed(self):
  316.         self.lastViewed = datetime.now() 
  317.         for item in self.items:
  318.             if item.getState() == "new":
  319.                 item.signalChange(needsSave=False)
  320.  
  321.         self.ufeed.signalChange()
  322.  
  323.     ##
  324.     # Returns true iff the feed is loading. Only makes sense in the
  325.     # context of UniversalFeeds
  326.     def isLoading(self):
  327.         return False
  328.  
  329.     ##
  330.     # Returns true iff this feed has a library
  331.     def hasLibrary(self):
  332.         return False
  333.  
  334.     def startManualDownload(self):
  335.         next = None
  336.         for item in self.items:
  337.             if item.isPendingManualDownload():
  338.                 if next is None:
  339.                     next = item
  340.                 elif item.getPubDateParsed() > next.getPubDateParsed():
  341.                     next = item
  342.         if next is not None:
  343.             next.download(autodl = False)
  344.  
  345.     def startAutoDownload(self):
  346.         next = None
  347.         for item in self.items:
  348.             if item.isEligibleForAutoDownload():
  349.                 if next is None:
  350.                     next = item
  351.                 elif item.getPubDateParsed() > next.getPubDateParsed():
  352.                     next = item
  353.         if next is not None:
  354.             next.download(autodl = True)
  355.  
  356.     ##
  357.     # Returns marks expired items as expired
  358.     def expireItems(self):
  359.         for item in self.items:
  360.             expireTime = item.getExpirationTime()
  361.             if (item.getState() == 'expiring' and expireTime is not None and 
  362.                     expireTime < datetime.now()):
  363.                 item.executeExpire()
  364.  
  365.     ##
  366.     # Returns true iff feed should be visible
  367.     def isVisible(self):
  368.         self.ufeed.confirmDBThread()
  369.         return self.visible
  370.  
  371.     def signalItems (self):
  372.         for item in self.items:
  373.             item.signalChange(needsSave=False)
  374.  
  375.     ##
  376.     # Return the 'system' expiration delay, in days (can be < 1.0)
  377.     def getDefaultExpiration(self):
  378.         return float(config.get(prefs.EXPIRE_AFTER_X_DAYS))
  379.  
  380.     ##
  381.     # Returns the 'system' expiration delay as a formatted string
  382.     @returnsUnicode
  383.     def getFormattedDefaultExpiration(self):
  384.         expiration = self.getDefaultExpiration()
  385.         formattedExpiration = u''
  386.         if expiration < 0:
  387.             formattedExpiration = _('never')
  388.         elif expiration < 1.0:
  389.             formattedExpiration = _('%d hours') % int(expiration * 24.0)
  390.         elif expiration == 1:
  391.             formattedExpiration = _('%d day') % int(expiration)
  392.         elif expiration > 1 and expiration < 30:
  393.             formattedExpiration = _('%d days') % int(expiration)
  394.         elif expiration >= 30:
  395.             formattedExpiration = _('%d months') % int(expiration / 30)
  396.         return formattedExpiration
  397.  
  398.     ##
  399.     # Returns "feed," "system," or "never"
  400.     @returnsUnicode
  401.     def getExpirationType(self):
  402.         self.ufeed.confirmDBThread()
  403.         return self.ufeed.expire
  404.  
  405.     ##
  406.     # Returns"unlimited" or the maximum number of items this feed can fall behind
  407.     def getMaxFallBehind(self):
  408.         self.ufeed.confirmDBThread()
  409.         if self.ufeed.fallBehind < 0:
  410.             return u"unlimited"
  411.         else:
  412.             return self.ufeed.fallBehind
  413.  
  414.     ##
  415.     # Returns "unlimited" or the maximum number of items this feed wants
  416.     def getMaxNew(self):
  417.         self.ufeed.confirmDBThread()
  418.         if self.ufeed.maxNew < 0:
  419.             return u"unlimited"
  420.         else:
  421.             return self.ufeed.maxNew
  422.  
  423.     ##
  424.     # Returns the total absolute expiration time in hours.
  425.     # WARNING: 'system' and 'never' expiration types return 0
  426.     def getExpirationTime(self):
  427.         delta = None
  428.         self.ufeed.confirmDBThread()
  429.         expireAfterSetting = config.get(prefs.EXPIRE_AFTER_X_DAYS)
  430.         if (self.ufeed.expireTime is None or self.ufeed.expire == 'never' or 
  431.             (self.ufeed.expire == 'system' and expireAfterSetting <= 0)):
  432.             return 0
  433.         else:
  434.             return (self.ufeed.expireTime.days * 24 + 
  435.                     self.ufeed.expireTime.seconds / 3600)
  436.  
  437.     ##
  438.     # Returns the number of days until a video expires
  439.     def getExpireDays(self):
  440.         ret = 0
  441.         self.ufeed.confirmDBThread()
  442.         try:
  443.             return self.ufeed.expireTime.days
  444.         except:
  445.             return timedelta(days=config.get(prefs.EXPIRE_AFTER_X_DAYS)).days
  446.  
  447.     ##
  448.     # Returns the number of hours until a video expires
  449.     def getExpireHours(self):
  450.         ret = 0
  451.         self.ufeed.confirmDBThread()
  452.         try:
  453.             return int(self.ufeed.expireTime.seconds/3600)
  454.         except:
  455.             return int(timedelta(days=config.get(prefs.EXPIRE_AFTER_X_DAYS)).seconds/3600)
  456.  
  457.     def getExpires (self):
  458.         expireAfterSetting = config.get(prefs.EXPIRE_AFTER_X_DAYS)
  459.         return (self.ufeed.expireTime is None or self.ufeed.expire == 'never' or 
  460.                 (self.ufeed.expire == 'system' and expireAfterSetting <= 0))
  461.  
  462.     ##
  463.     # Returns true iff item is autodownloadable
  464.     def isAutoDownloadable(self):
  465.         self.ufeed.confirmDBThread()
  466.         return self.ufeed.autoDownloadable
  467.  
  468.     def autoDownloadStatus(self):
  469.         status = self.isAutoDownloadable()
  470.         if status:
  471.             return u"ON"
  472.         else:
  473.             return u"OFF"
  474.  
  475.     ##
  476.     # Returns the title of the feed
  477.     @returnsUnicode
  478.     def getTitle(self):
  479.         try:
  480.             title = self.title
  481.             if whitespacePattern.match(title):
  482.                 title = self.url
  483.             return title
  484.         except:
  485.             return u""
  486.  
  487.     ##
  488.     # Returns the URL of the feed
  489.     @returnsUnicode
  490.     def getURL(self):
  491.         try:
  492.             if self.ufeed.searchTerm is None:
  493.                 return self.url
  494.             else:
  495.                 return u"dtv:searchTerm:%s?%s" % (urlencode(self.url), urlencode(self.ufeed.searchTerm))
  496.         except:
  497.             return u""
  498.  
  499.     ##
  500.     # Returns the URL of the feed
  501.     @returnsUnicode
  502.     def getBaseURL(self):
  503.         try:
  504.             return self.url
  505.         except:
  506.             return u""
  507.  
  508.     ##
  509.     # Returns the description of the feed
  510.     @returnsUnicode
  511.     def getDescription(self):
  512.         return u"<span />"
  513.  
  514.     ##
  515.     # Returns a link to a webpage associated with the feed
  516.     @returnsUnicode
  517.     def getLink(self):
  518.         return self.ufeed.getBaseHref()
  519.  
  520.     ##
  521.     # Returns the URL of the library associated with the feed
  522.     @returnsUnicode
  523.     def getLibraryLink(self):
  524.         return u""
  525.  
  526.     ##
  527.     # Returns the URL of a thumbnail associated with the feed
  528.     @returnsUnicode
  529.     def getThumbnailURL(self):
  530.         return self.thumbURL
  531.  
  532.     # See item.getThumbnail to figure out which items to send signals for.
  533.     def iconChanged(self, needsSave=True):
  534.         self.ufeed.signalChange(needsSave=needsSave)
  535.         for item in self.items:
  536.             if not (item.iconCache.isValid() or
  537.                     item.screenshot or
  538.                     item.isContainerItem):
  539.                 item.signalChange(needsSave=False)
  540.  
  541.     ##
  542.     # Returns URL of license assocaited with the feed
  543.     @returnsUnicode
  544.     def getLicense(self):
  545.         return u""
  546.  
  547.     ##
  548.     # Returns the number of new items with the feed
  549.     def getNewItems(self):
  550.         self.ufeed.confirmDBThread()
  551.         count = 0
  552.         for item in self.items:
  553.             try:
  554.                 if item.getState() == u'newly-downloaded':
  555.                     count += 1
  556.             except:
  557.                 pass
  558.         return count
  559.  
  560.     def onRestore(self):        
  561.         self.updating = False
  562.         self.calc_item_list()
  563.  
  564.     def onRemove(self):
  565.         """Called when the feed uses this FeedImpl is removed from the DB.
  566.         subclasses can perform cleanup here."""
  567.         pass
  568.  
  569.     def __str__(self):
  570.         return "FeedImpl - %s" % self.getTitle()
  571.  
  572. ##
  573. # This class is a magic class that can become any type of feed it wants
  574. #
  575. # It works by passing on attributes to the actual feed.
  576. class Feed(DDBObject):
  577.     ICON_CACHE_SIZES = [
  578.         (20, 20),
  579.         (76, 76),
  580.     ] + Item.ICON_CACHE_SIZES
  581.  
  582.     def __init__(self,url, initiallyAutoDownloadable=True):
  583.         DDBObject.__init__(self, add=False)
  584.         checkU(url)
  585.         self.autoDownloadable = initiallyAutoDownloadable
  586.         self.getEverything = False
  587.         self.maxNew = 3
  588.         self.expire = u"system"
  589.         self.expireTime = None
  590.         self.fallBehind = -1
  591.  
  592.         self.origURL = url
  593.         self.errorState = False
  594.         self.loading = True
  595.         self.actualFeed = FeedImpl(url,self)
  596.         self.iconCache = IconCache(self, is_vital = True)
  597.         self.informOnError = True
  598.         self.folder_id = None
  599.         self.searchTerm = None
  600.         self.userTitle = None
  601.         self._initRestore()
  602.         self.dd.addAfterCursor(self)
  603.         self.generateFeed(True)
  604.  
  605.     def signalChange (self, needsSave=True, needsSignalFolder=False):
  606.         if needsSignalFolder:
  607.             folder = self.getFolder()
  608.             if folder:
  609.                 folder.signalChange(needsSave=False)
  610.         DDBObject.signalChange (self, needsSave=needsSave)
  611.  
  612.     def _initRestore(self):
  613.         self.download = None
  614.         self.blinking = False
  615.         self.itemSort = sorts.ItemSort()
  616.         self.itemSortDownloading = sorts.ItemSort()
  617.         self.itemSortWatchable = sorts.ItemSortUnwatchedFirst()
  618.         self.inlineSearchTerm = None
  619.  
  620.     isBlinking, setBlinking = makeSimpleGetSet('blinking',
  621.             changeNeedsSave=False)
  622.  
  623.     def setInlineSearchTerm(self, term):
  624.         self.inlineSearchTerm = term
  625.  
  626.     def blink(self):
  627.         self.setBlinking(True)
  628.         def timeout():
  629.             if self.idExists():
  630.                 self.setBlinking(False)
  631.         eventloop.addTimeout(0.5, timeout, 'unblink feed')
  632.  
  633.     # Returns the ID of this feed. Deprecated.
  634.     def getFeedID(self):
  635.         return self.getID()
  636.  
  637.     def getID(self):
  638.         return DDBObject.getID(self)
  639.  
  640.     def hasError(self):
  641.         self.confirmDBThread()
  642.         return self.errorState
  643.  
  644.     @returnsUnicode
  645.     def getOriginalURL(self):
  646.         self.confirmDBThread()
  647.         return self.origURL
  648.  
  649.     @returnsUnicode
  650.     def getSearchTerm(self):
  651.         self.confirmDBThread()
  652.         return self.searchTerm
  653.  
  654.     @returnsUnicode
  655.     def getError(self):
  656.         return u"Could not load feed"
  657.  
  658.     def isUpdating(self):
  659.         return self.loading or (self.actualFeed and self.actualFeed.updating)
  660.  
  661.     def isScraped(self):
  662.         return isinstance(self.actualFeed, ScraperFeedImpl)
  663.  
  664.     @returnsUnicode
  665.     def getTitle(self):
  666.         if self.userTitle is None:
  667.             title = self.actualFeed.getTitle()
  668.             if self.searchTerm is not None:
  669.                 title = u"'%s' on %s" % (self.searchTerm, title)
  670.             return title
  671.         else:
  672.             return self.userTitle
  673.  
  674.     def setTitle(self, title):
  675.         self.confirmDBThread()
  676.         self.userTitle = title
  677.         self.signalChange()
  678.  
  679.     def unsetTitle(self):
  680.         self.setTitle(None)
  681.  
  682.     @returnsUnicode
  683.     def getAutoDownloadMode(self):
  684.         self.confirmDBThread()
  685.         if self.autoDownloadable:
  686.             if self.getEverything:
  687.                 return u'all'
  688.             else:
  689.                 return u'new'
  690.         else:
  691.             return u'off'
  692.  
  693.     def setAutoDownloadMode(self, mode):
  694.         if mode == u'all':
  695.             self.setGetEverything(True)
  696.             self.setAutoDownloadable(True)
  697.         elif mode == u'new':
  698.             self.setGetEverything(False)
  699.             self.setAutoDownloadable(True)
  700.         elif mode == u'off':
  701.             self.setAutoDownloadable(False)
  702.         else:
  703.             raise ValueError("Bad auto-download mode: %s" % mode)
  704.  
  705.     def getCurrentAutoDownloadableItems(self):
  706.         auto = set()
  707.         for item in self.items:
  708.             if item.isPendingAutoDownload():
  709.                 auto.add(item)
  710.         return auto
  711.  
  712.     ##
  713.     # Switch the auto-downloadable state
  714.     def setAutoDownloadable(self, automatic):
  715.         self.confirmDBThread()
  716.         if self.autoDownloadable == automatic:
  717.             return
  718.         self.autoDownloadable = automatic
  719.  
  720.         if self.autoDownloadable:
  721.             # When turning on auto-download, existing items shouldn't be
  722.             # considered "new"
  723.             for item in self.items:
  724.                 if item.eligibleForAutoDownload:
  725.                     item.eligibleForAutoDownload = False
  726.                     item.signalChange()
  727.  
  728.         for item in self.items:
  729.             if item.isEligibleForAutoDownload():
  730.                 item.signalChange(needsSave=False)
  731.  
  732.         self.signalChange()
  733.  
  734.     ##
  735.     # Sets the 'getEverything' attribute, True or False
  736.     def setGetEverything(self, everything):
  737.         self.confirmDBThread()
  738.         if everything == self.getEverything:
  739.             return
  740.         if not self.autoDownloadable:
  741.             self.getEverything = everything
  742.             self.signalChange()
  743.             return
  744.  
  745.         updates = set()
  746.         if everything:
  747.             for item in self.items:
  748.                 if not item.isEligibleForAutoDownload():
  749.                     updates.add(item)
  750.         else:
  751.             for item in self.items:
  752.                 if item.isEligibleForAutoDownload():
  753.                     updates.add(item)
  754.  
  755.         self.getEverything = everything
  756.         self.signalChange()
  757.  
  758.         if everything:
  759.             for item in updates:
  760.                 if item.isEligibleForAutoDownload():
  761.                     item.signalChange(needsSave=False)
  762.         else:
  763.             for item in updates:
  764.                 if not item.isEligibleForAutoDownload():
  765.                     item.signalChange(needsSave=False)
  766.  
  767.     ##
  768.     # Sets the expiration attributes. Valid types are 'system', 'feed' and 'never'
  769.     # Expiration time is in hour(s).
  770.     def setExpiration(self, type, time):
  771.         self.confirmDBThread()
  772.         self.expire = type
  773.         self.expireTime = timedelta(hours=time)
  774.  
  775.         if self.expire == "never":
  776.             for item in self.items:
  777.                 if item.isDownloaded():
  778.                     item.save()
  779.  
  780.         self.signalChange()
  781.         for item in self.items:
  782.             item.signalChange(needsSave=False)
  783.  
  784.     ##
  785.     # Sets the maxNew attributes. -1 means unlimited.
  786.     def setMaxNew(self, maxNew):
  787.         self.confirmDBThread()
  788.         oldMaxNew = self.maxNew
  789.         self.maxNew = maxNew
  790.         self.signalChange()
  791. #        for item in self.items:
  792. #            item.signalChange(needsSave=False)
  793.         if self.maxNew >= oldMaxNew or self.maxNew < 0:
  794.             import autodler
  795.             autodler.autoDownloader.startDownloads()
  796.  
  797.     def makeContextMenu(self, templateName, view):
  798.         items = [
  799.             (self.update, _('Update Channel Now')),
  800.             (lambda: app.delegate.copyTextToClipboard(self.getURL()),
  801.                 _('Copy URL to clipboard')),
  802.             (self.rename, _('Rename Channel')),
  803.         ]
  804.  
  805.         if self.userTitle:
  806.             items.append((self.unsetTitle, _('Revert Title to Default')))
  807.         items.append((lambda: app.controller.removeFeed(self), _('Remove')))
  808.         return menu.makeMenu(items)
  809.  
  810.     def rename(self):
  811.         title = _("Rename Channel")
  812.         text = _("Enter a new name for the channel %s" % self.getTitle())
  813.         def callback(dialog):
  814.             if self.idExists() and dialog.choice == dialogs.BUTTON_OK:
  815.                 self.setTitle(dialog.value)
  816.         dialogs.TextEntryDialog(title, text, dialogs.BUTTON_OK,
  817.             dialogs.BUTTON_CANCEL, prefillCallback=lambda:self.getTitle()).run(callback)
  818.  
  819.     def update(self):
  820.         self.confirmDBThread()
  821.         if not self.idExists():
  822.             return
  823.         if self.loading:
  824.             return
  825.         elif self.errorState:
  826.             self.loading = True
  827.             self.errorState = False
  828.             self.signalChange()
  829.             return self.generateFeed()
  830.         self.actualFeed.update()
  831.  
  832.     def getFolder(self):
  833.         self.confirmDBThread()
  834.         if self.folder_id is not None:
  835.             return self.dd.getObjectByID(self.folder_id)
  836.         else:
  837.             return None
  838.  
  839.     def setFolder(self, newFolder):
  840.         self.confirmDBThread()
  841.         oldFolder = self.getFolder()
  842.         if newFolder is not None:
  843.             self.folder_id = newFolder.getID()
  844.         else:
  845.             self.folder_id = None
  846.         self.signalChange()
  847.         for item in self.items:
  848.             item.signalChange(needsSave=False, needsUpdateXML=False)
  849.         if newFolder:
  850.             newFolder.signalChange(needsSave=False)
  851.         if oldFolder:
  852.             oldFolder.signalChange(needsSave=False)
  853.  
  854.     def generateFeed(self, removeOnError=False):
  855.         newFeed = None
  856.         if (self.origURL == u"dtv:directoryfeed"):
  857.             newFeed = DirectoryFeedImpl(self)
  858.         elif (self.origURL.startswith(u"dtv:directoryfeed:")):
  859.             url = self.origURL[len(u"dtv:directoryfeed:"):]
  860.             dir = unmakeURLSafe(url)
  861.             newFeed = DirectoryWatchFeedImpl(self, dir)
  862.         elif (self.origURL == u"dtv:search"):
  863.             newFeed = SearchFeedImpl(self)
  864.         elif (self.origURL == u"dtv:searchDownloads"):
  865.             newFeed = SearchDownloadsFeedImpl(self)
  866.         elif (self.origURL == u"dtv:manualFeed"):
  867.             newFeed = ManualFeedImpl(self)
  868.         elif (self.origURL == u"dtv:singleFeed"):
  869.             newFeed = SingleFeedImpl(self)
  870.         elif (self.origURL.startswith (u"dtv:multi:")):
  871.             newFeed = RSSMultiFeedImpl(self.origURL, self)
  872.         elif (self.origURL.startswith (u"dtv:searchTerm:")):
  873.  
  874.             url = self.origURL[len(u"dtv:searchTerm:"):]
  875.             (url, search) = url.rsplit("?", 1)
  876.             url = urldecode(url)
  877.             # search terms encoded as utf-8, but our URL attribute is then
  878.             # converted to unicode.  So we need to:
  879.             #  - convert the unicode to a raw string
  880.             #  - urldecode that string
  881.             #  - utf-8 decode the result.
  882.             search = urldecode(search.encode('ascii')).decode('utf-8')
  883.             self.searchTerm = search
  884.             if url.startswith (u"dtv:multi:"):
  885.                 newFeed = RSSMultiFeedImpl(url, self)
  886.             else:
  887.                 self.download = grabURL(url,
  888.                         lambda info:self._generateFeedCallback(info, removeOnError),
  889.                         lambda error:self._generateFeedErrback(error, removeOnError),
  890.                         defaultMimeType=u'application/rss+xml')
  891.         else:
  892.             self.download = grabURL(self.origURL,
  893.                     lambda info:self._generateFeedCallback(info, removeOnError),
  894.                     lambda error:self._generateFeedErrback(error, removeOnError),
  895.                     defaultMimeType=u'application/rss+xml')
  896.             logging.debug ("added async callback to create feed %s", self.origURL)
  897.         if newFeed:
  898.             self.actualFeed = newFeed
  899.             self.loading = False
  900.  
  901.             self.signalChange()
  902.  
  903.     def _handleFeedLoadingError(self, errorDescription):
  904.         self.download = None
  905.         self.errorState = True
  906.         self.loading = False
  907.         self.signalChange()
  908.         if self.informOnError:
  909.             title = _('Error loading feed')
  910.             description = _("Couldn't load the feed at %s (%s).") % (
  911.                     self.url, errorDescription)
  912.             description += "\n\n"
  913.             description += _("Would you like to keep the feed?")
  914.             d = dialogs.ChoiceDialog(title, description, dialogs.BUTTON_KEEP,
  915.                     dialogs.BUTTON_DELETE)
  916.             def callback(dialog):
  917.                 if dialog.choice == dialogs.BUTTON_DELETE and self.idExists():
  918.                     self.remove()
  919.             d.run(callback)
  920.             self.informOnError = False
  921.         delay = config.get(prefs.CHECK_CHANNELS_EVERY_X_MN)
  922.         eventloop.addTimeout(delay, self.update, "update failed feed")
  923.  
  924.     def _generateFeedErrback(self, error, removeOnError):
  925.         if not self.idExists():
  926.             return
  927.         logging.info ("Warning couldn't load feed at %s (%s)",
  928.                       self.origURL, error)
  929.         self._handleFeedLoadingError(error.getFriendlyDescription())
  930.  
  931.     def _generateFeedCallback(self, info, removeOnError):
  932.         """This is called by grabURL to generate a feed based on
  933.         the type of data found at the given URL
  934.         """
  935.         # FIXME: This probably should be split up a bit. The logic is
  936.         #        a bit daunting
  937.  
  938.  
  939.         # Note that all of the raw XML and HTML in this function is in
  940.         # byte string format
  941.  
  942.         if not self.idExists():
  943.             return
  944.         self.download = None
  945.         modified = unicodify(info.get('last-modified'))
  946.         etag = unicodify(info.get('etag'))
  947.         contentType = unicodify(info.get('content-type', u'text/html'))
  948.         
  949.         # Some smarty pants serve RSS feeds with a text/html content-type...
  950.         # So let's do some really simple sniffing first.
  951.         apparentlyRSS = re.compile(r'<\?xml.*\?>\s*<rss').match(info['body']) is not None
  952.  
  953.         #Definitely an HTML feed
  954.         if (contentType.startswith(u'text/html') or 
  955.             contentType.startswith(u'application/xhtml+xml')) and not apparentlyRSS:
  956.             #print "Scraping HTML"
  957.             html = info['body']
  958.             if info.has_key('charset'):
  959.                 html = fixHTMLHeader(html,info['charset'])
  960.                 charset = unicodify(info['charset'])
  961.             else:
  962.                 charset = None
  963.             self.askForScrape(info, html, charset)
  964.         #It's some sort of feed we don't know how to scrape
  965.         elif (contentType.startswith(u'application/rdf+xml') or
  966.               contentType.startswith(u'application/atom+xml')):
  967.             #print "ATOM or RDF"
  968.             html = info['body']
  969.             if info.has_key('charset'):
  970.                 xmldata = fixXMLHeader(html,info['charset'])
  971.             else:
  972.                 xmldata = html
  973.             self.finishGenerateFeed(RSSFeedImpl(unicodify(info['updated-url']),
  974.                 initialHTML=xmldata,etag=etag,modified=modified, ufeed=self))
  975.             # If it's not HTML, we can't be sure what it is.
  976.             #
  977.             # If we get generic XML, it's probably RSS, but it still could
  978.             # be XHTML.
  979.             #
  980.             # application/rss+xml links are definitely feeds. However, they
  981.             # might be pre-enclosure RSS, so we still have to download them
  982.             # and parse them before we can deal with them correctly.
  983.         elif (apparentlyRSS or
  984.               contentType.startswith(u'application/rss+xml') or
  985.               contentType.startswith(u'application/podcast+xml') or
  986.               contentType.startswith(u'text/xml') or 
  987.               contentType.startswith(u'application/xml') or
  988.               (contentType.startswith(u'text/plain') and
  989.                (unicodify(info['updated-url']).endswith(u'.xml') or
  990.                 unicodify(info['updated-url']).endswith(u'.rss')))):
  991.             #print " It's doesn't look like HTML..."
  992.             html = info["body"]
  993.             if info.has_key('charset'):
  994.                 xmldata = fixXMLHeader(html,info['charset'])
  995.                 html = fixHTMLHeader(html,info['charset'])
  996.                 charset = unicodify(info['charset'])
  997.             else:
  998.                 xmldata = html
  999.                 charset = None
  1000.             # FIXME html and xmldata can be non-unicode at this point
  1001.             parser = xml.sax.make_parser()
  1002.             parser.setFeature(xml.sax.handler.feature_namespaces, 1)
  1003.             try: parser.setFeature(xml.sax.handler.feature_external_ges, 0)
  1004.             except: pass
  1005.             handler = RSSLinkGrabber(unicodify(info['redirected-url']),charset)
  1006.             parser.setContentHandler(handler)
  1007.             parser.setErrorHandler(handler)
  1008.             try:
  1009.                 parser.parse(StringIO(xmldata))
  1010.             except UnicodeDecodeError:
  1011.                 logging.exception ("Unicode issue parsing... %s", xmldata[0:300])
  1012.                 self.finishGenerateFeed(None)
  1013.                 if removeOnError:
  1014.                     self.remove()
  1015.             except:
  1016.                 #it doesn't parse as RSS, so it must be HTML
  1017.                 #print " Nevermind! it's HTML"
  1018.                 self.askForScrape(info, html, charset)
  1019.             else:
  1020.                 #print " It's RSS with enclosures"
  1021.                 self.finishGenerateFeed(RSSFeedImpl(
  1022.                     unicodify(info['updated-url']),
  1023.                     initialHTML=xmldata, etag=etag, modified=modified,
  1024.                     ufeed=self))
  1025.         else:
  1026.             self._handleFeedLoadingError(_("Bad content-type"))
  1027.  
  1028.     def finishGenerateFeed(self, feedImpl):
  1029.         self.confirmDBThread()
  1030.         self.loading = False
  1031.         if feedImpl is not None:
  1032.             self.actualFeed = feedImpl
  1033.             self.errorState = False
  1034.         else:
  1035.             self.errorState = True
  1036.         self.signalChange()
  1037.  
  1038.     def askForScrape(self, info, initialHTML, charset):
  1039.         title = Template(_("Channel is not compatible with $shortAppName!")).substitute(shortAppName=config.get(prefs.SHORT_APP_NAME))
  1040.         descriptionTemplate = Template(_("""\
  1041. But we'll try our best to grab the files. It may take extra time to list the \
  1042. videos, and descriptions may look funny.  Please contact the publishers of \
  1043. $url and ask if they can supply a feed in a format that will work with \
  1044. $shortAppName.\n\nDo you want to try to load this channel anyway?"""))
  1045.         description = descriptionTemplate.substitute(url=info['updated-url'],
  1046.                                 shortAppName=config.get(prefs.SHORT_APP_NAME))
  1047.         dialog = dialogs.ChoiceDialog(title, description, dialogs.BUTTON_YES,
  1048.                 dialogs.BUTTON_NO)
  1049.  
  1050.         def callback(dialog):
  1051.             if not self.idExists():
  1052.                 return
  1053.             if dialog.choice == dialogs.BUTTON_YES:
  1054.                 uinfo = unicodify(info)
  1055.                 impl = ScraperFeedImpl(uinfo['updated-url'],
  1056.                     initialHTML=initialHTML, etag=uinfo.get('etag'),
  1057.                     modified=uinfo.get('modified'), charset=charset,
  1058.                     ufeed=self) 
  1059.                 self.finishGenerateFeed(impl)
  1060.             else:
  1061.                 self.remove()
  1062.         dialog.run(callback)
  1063.  
  1064.     def getActualFeed(self):
  1065.         return self.actualFeed
  1066.  
  1067.     def __getattr__(self,attr):
  1068.         return getattr(self.actualFeed,attr)
  1069.  
  1070.     def remove(self, moveItemsTo=None):
  1071.         """Remove the feed.  If moveItemsTo is None (the default), the items
  1072.         in this feed will be removed too.  If moveItemsTo is given, the items
  1073.         in this feed will be moved to that feed.
  1074.         """
  1075.  
  1076.         self.confirmDBThread()
  1077.  
  1078.         if isinstance (self.actualFeed, DirectoryWatchFeedImpl):
  1079.             moveItemsTo = None
  1080.         self.cancelUpdateEvents()
  1081.         if self.download is not None:
  1082.             self.download.cancel()
  1083.             self.download = None
  1084.         for item in self.items:
  1085.             if moveItemsTo is not None and item.isDownloaded():
  1086.                 item.setFeed(moveItemsTo.getID())
  1087.             else:
  1088.                 item.remove()
  1089.         if self.iconCache is not None:
  1090.             self.iconCache.remove()
  1091.             self.iconCache = None
  1092.         DDBObject.remove(self)
  1093.         self.actualFeed.onRemove()
  1094.  
  1095.     @returnsUnicode
  1096.     def getThumbnail(self):
  1097.         self.confirmDBThread()
  1098.         if self.iconCache and self.iconCache.isValid():
  1099.             path = self.iconCache.getResizedFilename(76, 76)
  1100.             return resources.absoluteUrl(path)
  1101.         else:
  1102.             return defaultFeedIconURL()
  1103.  
  1104.     @returnsUnicode
  1105.     def getTablistThumbnail(self):
  1106.         self.confirmDBThread()
  1107.         if self.iconCache and self.iconCache.isValid():
  1108.             path = self.iconCache.getResizedFilename(20, 20)
  1109.             return resources.absoluteUrl(path)
  1110.         else:
  1111.             return defaultFeedIconURLTablist()
  1112.  
  1113.     @returnsUnicode
  1114.     def getItemThumbnail(self, width, height):
  1115.         self.confirmDBThread()
  1116.         if self.iconCache and self.iconCache.isValid():
  1117.             path = self.iconCache.getResizedFilename(width, height)
  1118.             return resources.absoluteUrl(path)
  1119.         else:
  1120.             return None
  1121.  
  1122.     def hasDownloadedItems(self):
  1123.         self.confirmDBThread()
  1124.         for item in self.items:
  1125.             if item.isDownloaded():
  1126.                 return True
  1127.         return False
  1128.  
  1129.     def hasDownloadingItems(self):
  1130.         self.confirmDBThread()
  1131.         for item in self.items:
  1132.             if item.getState() in (u'downloading', u'paused'):
  1133.                 return True
  1134.         return False
  1135.  
  1136.     def updateIcons(self):
  1137.         iconCacheUpdater.clearVital()
  1138.         for item in self.items:
  1139.             item.iconCache.requestUpdate(True)
  1140.         for feed in views.feeds:
  1141.             feed.iconCache.requestUpdate(True)
  1142.  
  1143.     @returnsUnicode
  1144.     def getDragDestType(self):
  1145.         self.confirmDBThread()
  1146.         if self.folder_id is not None:
  1147.             return u'channel'
  1148.         else:
  1149.             return u'channel:channelfolder'
  1150.  
  1151.     def onRestore(self):
  1152.         if (self.iconCache == None):
  1153.             self.iconCache = IconCache (self, is_vital = True)
  1154.         else:
  1155.             self.iconCache.dbItem = self
  1156.             self.iconCache.requestUpdate(True)
  1157.         self.informOnError = False
  1158.         self._initRestore()
  1159.         if self.actualFeed.__class__ == FeedImpl:
  1160.             # Our initial FeedImpl was never updated, call generateFeed again
  1161.             self.loading = True
  1162.             eventloop.addIdle(lambda:self.generateFeed(True), "generateFeed")
  1163.  
  1164.     def __str__(self):
  1165.         return "Feed - %s" % self.getTitle()
  1166.  
  1167. def _entry_equal(a, b):
  1168.     if type(a) == list and type(b) == list:
  1169.         if len(a) != len(b):
  1170.             return False
  1171.         for i in xrange (len(a)):
  1172.             if not _entry_equal(a[i], b[i]):
  1173.                 return False
  1174.         return True
  1175.     try:
  1176.         return a.equal(b)
  1177.     except:
  1178.         try:
  1179.             return b.equal(a)
  1180.         except:
  1181.             return a == b
  1182.  
  1183. class RSSFeedImpl(FeedImpl):
  1184.     firstImageRE = re.compile('\<\s*img\s+[^>]*src\s*=\s*"(.*?)"[^>]*\>',re.I|re.M)
  1185.     
  1186.     def __init__(self,url,ufeed,title = None,initialHTML = None, etag = None, modified = None, visible=True):
  1187.         FeedImpl.__init__(self,url,ufeed,title,visible=visible)
  1188.         self.initialHTML = initialHTML
  1189.         self.etag = etag
  1190.         self.modified = modified
  1191.         self.download = None
  1192.         self.scheduleUpdateEvents(0)
  1193.  
  1194.     @returnsUnicode
  1195.     def getBaseHref(self):
  1196.         try:
  1197.             return escape(self.parsed.link)
  1198.         except:
  1199.             return FeedImpl.getBaseHref(self)
  1200.  
  1201.     ##
  1202.     # Returns the description of the feed
  1203.     @returnsUnicode
  1204.     def getDescription(self):
  1205.         self.ufeed.confirmDBThread()
  1206.         try:
  1207.             return xhtmlify(u'<span>'+unescape(self.parsed.feed.description)+u'</span>')
  1208.         except:
  1209.             return u"<span />"
  1210.  
  1211.     ##
  1212.     # Returns a link to a webpage associated with the feed
  1213.     @returnsUnicode
  1214.     def getLink(self):
  1215.         self.ufeed.confirmDBThread()
  1216.         try:
  1217.             return self.parsed.link
  1218.         except:
  1219.             return u""
  1220.  
  1221.     ##
  1222.     # Returns the URL of the library associated with the feed
  1223.     @returnsUnicode
  1224.     def getLibraryLink(self):
  1225.         self.ufeed.confirmDBThread()
  1226.         try:
  1227.             return self.parsed.libraryLink
  1228.         except:
  1229.             return u""
  1230.  
  1231.     def feedparser_finished (self):
  1232.         self.updating = False
  1233.         self.ufeed.signalChange(needsSave=False)
  1234.         self.scheduleUpdateEvents(-1)
  1235.  
  1236.     def feedparser_errback (self, e):
  1237.         if not self.ufeed.idExists():
  1238.             return
  1239.         logging.info ("Error updating feed: %s: %s", self.url, e)
  1240.         self.updating = False
  1241.         self.ufeed.signalChange()
  1242.         self.scheduleUpdateEvents(-1)
  1243.  
  1244.     def feedparser_callback (self, parsed):
  1245.         self.ufeed.confirmDBThread()
  1246.         if not self.ufeed.idExists():
  1247.             return
  1248.         start = clock()
  1249.         self.updateUsingParsed(parsed)
  1250.         self.feedparser_finished()
  1251.         end = clock()
  1252.         if end - start > 1.0:
  1253.             logging.timing ("feed update for: %s too slow (%.3f secs)", self.url, end - start)
  1254.  
  1255.     def call_feedparser (self, html):
  1256.         self.ufeed.confirmDBThread()
  1257.         in_thread = False
  1258.         if in_thread:
  1259.             try:
  1260.                 parsed = feedparser.parse(html)
  1261.                 self.updateUsingParsed(parsed)
  1262.             except:
  1263.                 logging.warning ("Error updating feed: %s", self.url)
  1264.                 self.updating = False
  1265.                 self.ufeed.signalChange(needsSave=False)
  1266.                 raise
  1267.             self.feedparser_finished()
  1268.         else:
  1269.             eventloop.callInThread (self.feedparser_callback, self.feedparser_errback, feedparser.parse, "Feedparser callback - %s" % self.url, html)
  1270.  
  1271.     ##
  1272.     # Updates a feed
  1273.     def update(self):
  1274.         self.ufeed.confirmDBThread()
  1275.         if not self.ufeed.idExists():
  1276.             return
  1277.         if self.updating:
  1278.             return
  1279.         else:
  1280.             self.updating = True
  1281.             self.ufeed.signalChange(needsSave=False)
  1282.         if hasattr(self, 'initialHTML') and self.initialHTML is not None:
  1283.             html = self.initialHTML
  1284.             self.initialHTML = None
  1285.             self.call_feedparser (html)
  1286.         else:
  1287.             try:
  1288.                 etag = self.etag
  1289.             except:
  1290.                 etag = None
  1291.             try:
  1292.                 modified = self.modified
  1293.             except:
  1294.                 modified = None
  1295.             self.download = grabURL(self.url, self._updateCallback,
  1296.                     self._updateErrback, etag=etag,modified=modified,defaultMimeType=u'application/rss+xml',)
  1297.  
  1298.     def _updateErrback(self, error):
  1299.         if not self.ufeed.idExists():
  1300.             return
  1301.         logging.info ("WARNING: error in Feed.update for %s -- %s", self.ufeed, error)
  1302.         self.scheduleUpdateEvents(-1)
  1303.         self.updating = False
  1304.         self.ufeed.signalChange(needsSave=False)
  1305.  
  1306.     def _updateCallback(self,info):
  1307.         if not self.ufeed.idExists():
  1308.             return
  1309.         if info.get('status') == 304:
  1310.             self.scheduleUpdateEvents(-1)
  1311.             self.updating = False
  1312.             self.ufeed.signalChange()
  1313.             return
  1314.         html = info['body']
  1315.         if info.has_key('charset'):
  1316.             html = fixXMLHeader(html,info['charset'])
  1317.  
  1318.         # FIXME HTML can be non-unicode here --NN        
  1319.         self.url = unicodify(info['updated-url'])
  1320.         if info.has_key('etag'):
  1321.             self.etag = unicodify(info['etag'])
  1322.         else:
  1323.             self.etag = None
  1324.         if info.has_key('last-modified'):
  1325.             self.modified = unicodify(info['last-modified'])
  1326.         else:
  1327.             self.modified = None
  1328.         self.call_feedparser (html)
  1329.  
  1330.     def _handleNewEntryForItem(self, item, entry, channelTitle):
  1331.         """Handle when we get a different entry for an item.
  1332.  
  1333.         This happens when the feed sets the RSS GUID attribute, then changes
  1334.         the entry for it.  Most of the time we will just update the item, but
  1335.         if the user has already downloaded the item then we need to make sure
  1336.         that we don't throw away the download.
  1337.         """
  1338.  
  1339.         videoEnc = getFirstVideoEnclosure(entry)
  1340.         if videoEnc is not None:
  1341.             entryURL = videoEnc.get('url')
  1342.         else:
  1343.             entryURL = None
  1344.         if item.isDownloaded() and item.getURL() != entryURL:
  1345.             item.removeRSSID()
  1346.             self._handleNewEntry(entry, channelTitle)
  1347.         else:
  1348.             item.update(entry)
  1349.  
  1350.     def _handleNewEntry(self, entry, channelTitle):
  1351.         """Handle getting a new entry from a feed."""
  1352.         item = Item(entry, feed_id=self.ufeed.id)
  1353.         if not filters.matchingItems(item, self.ufeed.searchTerm):
  1354.             item.remove()
  1355.         item.setChannelTitle(channelTitle)
  1356.  
  1357.     def updateUsingParsed(self, parsed):
  1358.         """Update the feed using parsed XML passed in"""
  1359.         self.parsed = unicodify(parsed)
  1360.  
  1361.         # This is a HACK for Yahoo! search which doesn't provide
  1362.         # enclosures
  1363.         for entry in parsed['entries']:
  1364.             if 'enclosures' not in entry:
  1365.                 try:
  1366.                     url = entry['link']
  1367.                 except:
  1368.                     continue
  1369.                 mimetype = filetypes.guessMimeType(url)
  1370.                 if mimetype is not None:
  1371.                     entry['enclosures'] = [{'url':toUni(url), 'type':toUni(mimetype)}]
  1372.                 else:
  1373.                     logging.info('unknown url type %s, not generating enclosure' % url)
  1374.  
  1375.         channelTitle = None
  1376.         try:
  1377.             channelTitle = self.parsed["feed"]["title"]
  1378.         except KeyError:
  1379.             try:
  1380.                 channelTitle = self.parsed["channel"]["title"]
  1381.             except KeyError:
  1382.                 pass
  1383.         if channelTitle != None:
  1384.             self.title = channelTitle
  1385.         if (self.parsed.feed.has_key('image') and 
  1386.             self.parsed.feed.image.has_key('url')):
  1387.             self.thumbURL = self.parsed.feed.image.url
  1388.             self.ufeed.iconCache.requestUpdate(is_vital=True)
  1389.         items_byid = {}
  1390.         items_byURLTitle = {}
  1391.         items_nokey = []
  1392.         old_items = set()
  1393.         for item in self.items:
  1394.             old_items.add(item)
  1395.             try:
  1396.                 items_byid[item.getRSSID()] = item
  1397.             except KeyError:
  1398.                 items_nokey.append (item)
  1399.             entry = item.getRSSEntry()
  1400.             videoEnc = getFirstVideoEnclosure(entry)
  1401.             if videoEnc is not None:
  1402.                 entryURL = videoEnc.get('url')
  1403.             else:
  1404.                 entryURL = None
  1405.             title = entry.get("title")
  1406.             if title is not None or entryURL is not None:
  1407.                 items_byURLTitle[(entryURL, title)] = item
  1408.         for entry in self.parsed.entries:
  1409.             entry = self.addScrapedThumbnail(entry)
  1410.             new = True
  1411.             if entry.has_key("id"):
  1412.                 id = entry["id"]
  1413.                 if items_byid.has_key (id):
  1414.                     item = items_byid[id]
  1415.                     if not _entry_equal(entry, item.getRSSEntry()):
  1416.                         self._handleNewEntryForItem(item, entry, channelTitle)
  1417.                     new = False
  1418.                     old_items.discard(item)
  1419.             if new:
  1420.                 videoEnc = getFirstVideoEnclosure(entry)
  1421.                 if videoEnc is not None:
  1422.                     entryURL = videoEnc.get('url')
  1423.                 else:
  1424.                     entryURL = None
  1425.                 title = entry.get("title")
  1426.                 if title is not None or entryURL is not None:
  1427.                     if items_byURLTitle.has_key ((entryURL, title)):
  1428.                         item = items_byURLTitle[(entryURL, title)]
  1429.                         if not _entry_equal(entry, item.getRSSEntry()):
  1430.                             self._handleNewEntryForItem(item, entry, channelTitle)
  1431.                         new = False
  1432.                         old_items.discard(item)
  1433.             if new:
  1434.                 for item in items_nokey:
  1435.                     if _entry_equal(entry, item.getRSSEntry()):
  1436.                         new = False
  1437.                     else:
  1438.                         try:
  1439.                             if _entry_equal (entry["enclosures"], item.getRSSEntry()["enclosures"]):
  1440.                                 self._handleNewEntryForItem(item, entry, channelTitle)
  1441.                                 new = False
  1442.                                 old_items.discard(item)
  1443.                         except:
  1444.                             pass
  1445.             if (new and entry.has_key('enclosures') and
  1446.                     getFirstVideoEnclosure(entry) != None):
  1447.                 self._handleNewEntry(entry, channelTitle)
  1448.         try:
  1449.             updateFreq = self.parsed["feed"]["ttl"]
  1450.         except KeyError:
  1451.             updateFreq = 0
  1452.         self.setUpdateFrequency(updateFreq)
  1453.         
  1454.         if self.initialUpdate:
  1455.             self.initialUpdate = False
  1456.             startfrom = None
  1457.             itemToUpdate = None
  1458.             for item in self.items:
  1459.                 itemTime = item.getPubDateParsed()
  1460.                 if startfrom is None or itemTime > startfrom:
  1461.                     startfrom = itemTime
  1462.                     itemToUpdate = item
  1463.             for item in self.items:
  1464.                 if item == itemToUpdate:
  1465.                     item.eligibleForAutoDownload = True
  1466.                 else:
  1467.                     item.eligibleForAutoDownload = False
  1468.                 item.signalChange()
  1469.             self.ufeed.signalChange()
  1470.  
  1471.         self.truncateOldItems(old_items)
  1472.  
  1473.     def truncateOldItems(self, old_items):
  1474.         """Truncate items so that the number of items in this feed doesn't
  1475.         exceed prefs.TRUNCATE_CHANNEL_AFTER_X_ITEMS.
  1476.  
  1477.         old_items should be an iterable that contains items that aren't in the
  1478.         feed anymore.
  1479.  
  1480.         Items are only truncated if they don't exist in the feed anymore, and
  1481.         if the user hasn't downloaded them.
  1482.         """
  1483.         limit = config.get(prefs.TRUNCATE_CHANNEL_AFTER_X_ITEMS)
  1484.         extra = len(self.items) - limit
  1485.         if extra <= 0:
  1486.             return
  1487.  
  1488.         candidates = []
  1489.         for item in old_items:
  1490.             if item.downloader is None:
  1491.                 candidates.append((item.creationTime, item))
  1492.         candidates.sort()
  1493.         for time, item in candidates[:extra]:
  1494.             item.remove()
  1495.  
  1496.     def addScrapedThumbnail(self,entry):
  1497.         # skip this if the entry already has a thumbnail.
  1498.         if entry.has_key('thumbnail'):
  1499.             return entry
  1500.         if entry.has_key('enclosures'):
  1501.             for enc in entry['enclosures']:
  1502.                 if enc.has_key('thumbnail'):
  1503.                     return entry
  1504.         # try to scape the thumbnail from the description.
  1505.         if not entry.has_key('description'):
  1506.             return entry
  1507.         desc = RSSFeedImpl.firstImageRE.search(unescape(entry['description']))
  1508.         if not desc is None:
  1509.             entry['thumbnail'] = FeedParserDict({'url': desc.expand("\\1")})
  1510.         return entry
  1511.  
  1512.     ##
  1513.     # Returns the URL of the license associated with the feed
  1514.     @returnsUnicode
  1515.     def getLicense(self):
  1516.         try:
  1517.             ret = self.parsed.license
  1518.         except:
  1519.             ret = u""
  1520.         return ret
  1521.  
  1522.     def onRemove(self):
  1523.         if self.download is not None:
  1524.             self.download.cancel()
  1525.             self.download = None
  1526.  
  1527.     ##
  1528.     # Called by pickle during deserialization
  1529.     def onRestore(self):
  1530.         #self.itemlist = defaultDatabase.filter(lambda x:isinstance(x,Item) and x.feed is self)
  1531.         #FIXME: the update dies if all of the items aren't restored, so we 
  1532.         # wait a little while before we start the update
  1533.         FeedImpl.onRestore(self)
  1534.         self.download = None
  1535.         self.scheduleUpdateEvents(0.1)
  1536.  
  1537. # FIXME: Derive from RSSFeedImpl, but that requires changing RSSFeedImpl some.
  1538. class RSSMultiFeedImpl(FeedImpl):
  1539.     firstImageRE = re.compile('\<\s*img\s+[^>]*src\s*=\s*"(.*?)"[^>]*\>',re.I|re.M)
  1540.     
  1541.     def __init__(self,url,ufeed,title = None, visible=True):
  1542.         FeedImpl.__init__(self,url,ufeed,title,visible=visible)
  1543.         self.etag = {}
  1544.         self.modified = {}
  1545.         self.download_dc = {}
  1546.         self.updating = 0
  1547.         self.splitURLs()
  1548.         self.scheduleUpdateEvents(0)
  1549.  
  1550.     def splitURLs(self):
  1551.         if self.url.startswith("dtv:multi:"):
  1552.             url = self.url[len("dtv:multi:"):]
  1553.             self.urls = [urldecode (x) for x in url.split(",")]
  1554.         else:
  1555.             self.urls = [self.url]
  1556.  
  1557.     ##
  1558.     # Returns the description of the feed
  1559.     @returnsUnicode
  1560.     def getDescription(self):
  1561.         self.ufeed.confirmDBThread()
  1562.         try:
  1563.             return u'<span>Search All</span>'
  1564.         except:
  1565.             return u"<span />"
  1566.  
  1567.     def checkUpdateFinished(self):
  1568.         if self.updating == 0:
  1569.             self.updateFinished()
  1570.  
  1571.     def updateFinished(self):
  1572.         if self.initialUpdate:
  1573.             self.initialUpdate = False
  1574.             startfrom = None
  1575.             itemToUpdate = None
  1576.             for item in self.items:
  1577.                 itemTime = item.getPubDateParsed()
  1578.                 if startfrom is None or itemTime > startfrom:
  1579.                     startfrom = itemTime
  1580.                     itemToUpdate = item
  1581.             for item in self.items:
  1582.                 if item == itemToUpdate:
  1583.                     item.eligibleForAutoDownload = True
  1584.                 else:
  1585.                     item.eligibleForAutoDownload = False
  1586.                 item.signalChange()
  1587.             self.ufeed.signalChange()
  1588.         self.ufeed.signalChange(needsSave=False)
  1589.  
  1590.     def feedparser_finished (self, url, needsSave = False):
  1591.         if not self.ufeed.idExists():
  1592.             return
  1593.         self.updating -= 1
  1594.         self.checkUpdateFinished()
  1595.         self.scheduleUpdateEvents(-1)
  1596.         del self.download_dc[url]
  1597.  
  1598.     def feedparser_errback (self, e, url):
  1599.         if not self.ufeed.idExists():
  1600.             return
  1601.         if e:
  1602.             logging.info ("Error updating feed: %s (%s): %s", self.url, url, e)
  1603.         else:
  1604.             logging.info ("Error updating feed: %s (%s)", self.url, url)
  1605.         self.feedparser_finished(url, True)
  1606.  
  1607.     def feedparser_callback (self, parsed, url):
  1608.         self.ufeed.confirmDBThread()
  1609.         if not self.ufeed.idExists():
  1610.             return
  1611.         start = clock()
  1612.         parsed = unicodify(parsed)
  1613.         self.updateUsingParsed(parsed)
  1614.         self.feedparser_finished(url)
  1615.         end = clock()
  1616.         if end - start > 1.0:
  1617.             logging.timing ("feed update for: %s too slow (%.3f secs)", self.url, end - start)
  1618.  
  1619.     def call_feedparser (self, html, url):
  1620.         self.ufeed.confirmDBThread()
  1621.         in_thread = False
  1622.         if in_thread:
  1623.             try:
  1624.                 parsed = feedparser.parse(html)
  1625.                 feedparser_callback(parsed, url)
  1626.             except:
  1627.                 self.feedparser_errback(self, None, url)
  1628.                 raise
  1629.         else:
  1630.             eventloop.callInThread (lambda parsed, url=url: self.feedparser_callback(parsed, url),
  1631.                                     lambda e, url=url: self.feedparser_errback(e, url),
  1632.                                     feedparser.parse, "Feedparser callback - %s" % url, html)
  1633.  
  1634.     ##
  1635.     # Updates a feed
  1636.     def update(self):
  1637.         self.ufeed.confirmDBThread()
  1638.         if not self.ufeed.idExists():
  1639.             return
  1640.         if self.updating:
  1641.             return
  1642.         for url in self.urls:
  1643.             try:
  1644.                 etag = self.etag[url]
  1645.             except:
  1646.                 etag = None
  1647.             try:
  1648.                 modified = self.modified[url]
  1649.             except:
  1650.                 modified = None
  1651.             self.download_dc[url] = grabURL(url,
  1652.                                             lambda x, url=url:self._updateCallback(x, url),
  1653.                                             lambda x, url=url:self._updateErrback(x, url),
  1654.                                             etag=etag, modified=modified,
  1655.                                             defaultMimeType=u'application/rss+xml',)
  1656.             self.updating += 1
  1657.  
  1658.     def _updateErrback(self, error, url):
  1659.         if not self.ufeed.idExists():
  1660.             return
  1661.         logging.info ("WARNING: error in Feed.update for %s (%s) -- %s", self.ufeed, url, error)
  1662.         self.scheduleUpdateEvents(-1)
  1663.         self.updating -= 1
  1664.         self.checkUpdateFinished()
  1665.         self.ufeed.signalChange(needsSave=False)
  1666.  
  1667.     def _updateCallback(self,info, url):
  1668.         if not self.ufeed.idExists():
  1669.             return
  1670.         if info.get('status') == 304:
  1671.             self.scheduleUpdateEvents(-1)
  1672.             self.updating -= 1 
  1673.             self.checkUpdateFinished()
  1674.             self.ufeed.signalChange()
  1675.             return
  1676.         html = info['body']
  1677.         if info.has_key('charset'):
  1678.             html = fixXMLHeader(html,info['charset'])
  1679.  
  1680.         # FIXME HTML can be non-unicode here --NN
  1681.         # FIXME How to update this properly
  1682.         #self.url = unicodify(info['updated-url'])
  1683.         if info.has_key('etag'):
  1684.             self.etag[url] = unicodify(info['etag'])
  1685.         else:
  1686.             self.etag[url] = None
  1687.         if info.has_key('last-modified'):
  1688.             self.modified[url] = unicodify(info['last-modified'])
  1689.         else:
  1690.             self.modified[url] = None
  1691.         self.call_feedparser (html, url)
  1692.  
  1693.     def _handleNewEntryForItem(self, item, entry, channelTitle):
  1694.         """Handle when we get a different entry for an item.
  1695.  
  1696.         This happens when the feed sets the RSS GUID attribute, then changes
  1697.         the entry for it.  Most of the time we will just update the item, but
  1698.         if the user has already downloaded the item then we need to make sure
  1699.         that we don't throw away the download.
  1700.         """
  1701.  
  1702.         videoEnc = getFirstVideoEnclosure(entry)
  1703.         if videoEnc is not None:
  1704.             entryURL = videoEnc.get('url')
  1705.         else:
  1706.             entryURL = None
  1707.         if item.isDownloaded() and item.getURL() != entryURL:
  1708.             item.removeRSSID()
  1709.             self._handleNewEntry(entry, channelTitle)
  1710.         else:
  1711.             item.update(entry)
  1712.  
  1713.     def _handleNewEntry(self, entry, channelTitle):
  1714.         """Handle getting a new entry from a feed."""
  1715.         item = Item(entry, feed_id=self.ufeed.id)
  1716.         if not filters.matchingItems(item, self.ufeed.searchTerm):
  1717.             item.remove()
  1718.         item.setChannelTitle(channelTitle)
  1719.  
  1720.     def updateUsingParsed(self, parsed):
  1721.         """Update the feed using parsed XML passed in"""
  1722.  
  1723.         # This is a HACK for Yahoo! search which doesn't provide
  1724.         # enclosures
  1725.         for entry in parsed['entries']:
  1726.             if 'enclosures' not in entry:
  1727.                 try:
  1728.                     url = entry['link']
  1729.                 except:
  1730.                     continue
  1731.                 mimetype = filetypes.guessMimeType(url)
  1732.                 if mimetype is not None:
  1733.                     entry['enclosures'] = [{'url':toUni(url), 'type':toUni(mimetype)}]
  1734.                 else:
  1735.                     logging.info('unknown url type %s, not generating enclosure' % url)
  1736.  
  1737.         channelTitle = None
  1738.         try:
  1739.             channelTitle = parsed["feed"]["title"]
  1740.         except KeyError:
  1741.             try:
  1742.                 channelTitle = parsed["channel"]["title"]
  1743.             except KeyError:
  1744.                 pass
  1745.         if not self.url.startswith("dtv:multi:"):
  1746.             if channelTitle != None:
  1747.                 self.title = channelTitle
  1748.             if (parsed.feed.has_key('image') and 
  1749.                 parsed.feed.image.has_key('url')):
  1750.                 self.thumbURL = parsed.feed.image.url
  1751.                 self.ufeed.iconCache.requestUpdate(is_vital=True)
  1752.  
  1753.         items_byid = {}
  1754.         items_byURLTitle = {}
  1755.         items_nokey = []
  1756.         old_items = set()
  1757.         for item in self.items:
  1758.             old_items.add(item)
  1759.             try:
  1760.                 items_byid[item.getRSSID()] = item
  1761.             except KeyError:
  1762.                 items_nokey.append (item)
  1763.             entry = item.getRSSEntry()
  1764.             videoEnc = getFirstVideoEnclosure(entry)
  1765.             if videoEnc is not None:
  1766.                 entryURL = videoEnc.get('url')
  1767.             else:
  1768.                 entryURL = None
  1769.             title = entry.get("title")
  1770.             if title is not None or entryURL is not None:
  1771.                 items_byURLTitle[(entryURL, title)] = item
  1772.         for entry in parsed.entries:
  1773.             entry = self.addScrapedThumbnail(entry)
  1774.             new = True
  1775.             if entry.has_key("id"):
  1776.                 id = entry["id"]
  1777.                 if items_byid.has_key (id):
  1778.                     item = items_byid[id]
  1779.                     if not _entry_equal(entry, item.getRSSEntry()):
  1780.                         self._handleNewEntryForItem(item, entry, channelTitle)
  1781.                     new = False
  1782.                     old_items.discard(item)
  1783.             if new:
  1784.                 videoEnc = getFirstVideoEnclosure(entry)
  1785.                 if videoEnc is not None:
  1786.                     entryURL = videoEnc.get('url')
  1787.                 else:
  1788.                     entryURL = None
  1789.                 title = entry.get("title")
  1790.                 if title is not None or entryURL is not None:
  1791.                     if items_byURLTitle.has_key ((entryURL, title)):
  1792.                         item = items_byURLTitle[(entryURL, title)]
  1793.                         if not _entry_equal(entry, item.getRSSEntry()):
  1794.                             self._handleNewEntryForItem(item, entry, channelTitle)
  1795.                         new = False
  1796.                         old_items.discard(item)
  1797.             if new:
  1798.                 for item in items_nokey:
  1799.                     if _entry_equal(entry, item.getRSSEntry()):
  1800.                         new = False
  1801.                     else:
  1802.                         try:
  1803.                             if _entry_equal (entry["enclosures"], item.getRSSEntry()["enclosures"]):
  1804.                                 self._handleNewEntryForItem(item, entry, channelTitle)
  1805.                                 new = False
  1806.                                 old_items.discard(item)
  1807.                         except:
  1808.                             pass
  1809.             if (new and entry.has_key('enclosures') and
  1810.                     getFirstVideoEnclosure(entry) != None):
  1811.                 self._handleNewEntry(entry, channelTitle)
  1812. #        try:
  1813. #            updateFreq = parsed["feed"]["ttl"]
  1814. #        except KeyError:
  1815. #            updateFreq = 0
  1816. #        self.setUpdateFrequency(updateFreq)
  1817.         
  1818.         self.truncateOldItems(old_items)
  1819.  
  1820.     def truncateOldItems(self, old_items):
  1821.         """Truncate items so that the number of items in this feed doesn't
  1822.         exceed prefs.TRUNCATE_CHANNEL_AFTER_X_ITEMS.
  1823.  
  1824.         old_items should be an iterable that contains items that aren't in the
  1825.         feed anymore.
  1826.  
  1827.         Items are only truncated if they don't exist in the feed anymore, and
  1828.         if the user hasn't downloaded them.
  1829.         """
  1830.         limit = config.get(prefs.TRUNCATE_CHANNEL_AFTER_X_ITEMS)
  1831.         extra = len(self.items) - limit
  1832.         if extra <= 0:
  1833.             return
  1834.  
  1835.         candidates = []
  1836.         for item in old_items:
  1837.             if item.downloader is None:
  1838.                 candidates.append((item.creationTime, item))
  1839.         candidates.sort()
  1840.         for time, item in candidates[:extra]:
  1841.             item.remove()
  1842.  
  1843.     def addScrapedThumbnail(self,entry):
  1844.         # skip this if the entry already has a thumbnail.
  1845.         if entry.has_key('thumbnail'):
  1846.             return entry
  1847.         if entry.has_key('enclosures'):
  1848.             for enc in entry['enclosures']:
  1849.                 if enc.has_key('thumbnail'):
  1850.                     return entry
  1851.         # try to scape the thumbnail from the description.
  1852.         if not entry.has_key('description'):
  1853.             return entry
  1854.         desc = RSSMultiFeedImpl.firstImageRE.search(unescape(entry['description']))
  1855.         if not desc is None:
  1856.             entry['thumbnail'] = FeedParserDict({'url': desc.expand("\\1")})
  1857.         return entry
  1858.  
  1859.     def onRemove(self):
  1860.         for dc in self.download_dc.values():
  1861.             if dc is not None:
  1862.                 dc.cancel()
  1863.         self.download_dc = {}
  1864.  
  1865.     ##
  1866.     # Called by pickle during deserialization
  1867.     def onRestore(self):
  1868.         #self.itemlist = defaultDatabase.filter(lambda x:isinstance(x,Item) and x.feed is self)
  1869.         #FIXME: the update dies if all of the items aren't restored, so we 
  1870.         # wait a little while before we start the update
  1871.         FeedImpl.onRestore(self)
  1872.         self.download_dc = {}
  1873.         self.updating = 0
  1874.         self.splitURLs()
  1875.         self.scheduleUpdateEvents(0.1)
  1876.  
  1877.  
  1878. ##
  1879. # A DTV Collection of items -- similar to a playlist
  1880. class Collection(FeedImpl):
  1881.     def __init__(self,ufeed,title = None):
  1882.         FeedImpl.__init__(self,ufeed,url = "dtv:collection",title = title,visible = False)
  1883.  
  1884.     ##
  1885.     # Adds an item to the collection
  1886.     def addItem(self,item):
  1887.         if isinstance(item,Item):
  1888.             self.ufeed.confirmDBThread()
  1889.             self.removeItem(item)
  1890.             self.items.append(item)
  1891.             return True
  1892.         else:
  1893.             return False
  1894.  
  1895.     ##
  1896.     # Moves an item to another spot in the collection
  1897.     def moveItem(self,item,pos):
  1898.         self.ufeed.confirmDBThread()
  1899.         self.removeItem(item)
  1900.         if pos < len(self.items):
  1901.             self.items[pos:pos] = [item]
  1902.         else:
  1903.             self.items.append(item)
  1904.  
  1905.     ##
  1906.     # Removes an item from the collection
  1907.     def removeItem(self,item):
  1908.         self.ufeed.confirmDBThread()
  1909.         for x in range(0,len(self.items)):
  1910.             if self.items[x] == item:
  1911.                 self.items[x:x+1] = []
  1912.                 break
  1913.         return True
  1914.  
  1915. ##
  1916. # A feed based on un unformatted HTML or pre-enclosure RSS
  1917. class ScraperFeedImpl(FeedImpl):
  1918.     def __init__(self,url,ufeed, title = None, visible = True, initialHTML = None,etag=None,modified = None,charset = None):
  1919.         FeedImpl.__init__(self,url,ufeed,title,visible)
  1920.         self.initialHTML = initialHTML
  1921.         self.initialCharset = charset
  1922.         self.linkHistory = {}
  1923.         self.linkHistory[url] = {}
  1924.         self.tempHistory = {}
  1925.         if not etag is None:
  1926.             self.linkHistory[url]['etag'] = unicodify(etag)
  1927.         if not modified is None:
  1928.             self.linkHistory[url]['modified'] = unicodify(modified)
  1929.         self.downloads = set()
  1930.         self.setUpdateFrequency(360)
  1931.         self.scheduleUpdateEvents(0)
  1932.  
  1933.     @returnsUnicode
  1934.     def getMimeType(self,link):
  1935.         raise StandardError, "ScraperFeedImpl.getMimeType not implemented"
  1936.  
  1937.     ##
  1938.     # This puts all of the caching information in tempHistory into the
  1939.     # linkHistory. This should be called at the end of an updated so that
  1940.     # the next time we update we don't unnecessarily follow old links
  1941.     def saveCacheHistory(self):
  1942.         self.ufeed.confirmDBThread()
  1943.         for url in self.tempHistory.keys():
  1944.             self.linkHistory[url] = self.tempHistory[url]
  1945.         self.tempHistory = {}
  1946.     ##
  1947.     # grabs HTML at the given URL, then processes it
  1948.     def getHTML(self, urlList, depth = 0, linkNumber = 0, top = False):
  1949.         url = urlList.pop(0)
  1950.         #print "Grabbing %s" % url
  1951.         etag = None
  1952.         modified = None
  1953.         if self.linkHistory.has_key(url):
  1954.             if self.linkHistory[url].has_key('etag'):
  1955.                 etag = self.linkHistory[url]['etag']
  1956.             if self.linkHistory[url].has_key('modified'):
  1957.                 modified = self.linkHistory[url]['modified']
  1958.         def callback(info):
  1959.             if not self.ufeed.idExists():
  1960.                 return
  1961.             self.downloads.discard(download)
  1962.             try:
  1963.                 self.processDownloadedHTML(info, urlList, depth,linkNumber, top)
  1964.             finally:
  1965.                 self.checkDone()
  1966.         def errback(error):
  1967.             if not self.ufeed.idExists():
  1968.                 return
  1969.             self.downloads.discard(download)
  1970.             logging.info ("WARNING unhandled error for ScraperFeedImpl.getHTML: %s", error)
  1971.             self.checkDone()
  1972.         download = grabURL(url, callback, errback, etag=etag,
  1973.                 modified=modified,defaultMimeType='text/html',)
  1974.         self.downloads.add(download)
  1975.  
  1976.     def processDownloadedHTML(self, info, urlList, depth, linkNumber, top = False):
  1977.         self.ufeed.confirmDBThread()
  1978.         #print "Done grabbing %s" % info['updated-url']
  1979.         
  1980.         if not self.tempHistory.has_key(info['updated-url']):
  1981.             self.tempHistory[info['updated-url']] = {}
  1982.         if info.has_key('etag'):
  1983.             self.tempHistory[info['updated-url']]['etag'] = unicodify(info['etag'])
  1984.         if info.has_key('last-modified'):
  1985.             self.tempHistory[info['updated-url']]['modified'] = unicodify(info['last-modified'])
  1986.  
  1987.         if (info['status'] != 304) and (info.has_key('body')):
  1988.             if info.has_key('charset'):
  1989.                 subLinks = self.scrapeLinks(info['body'], info['redirected-url'],charset=info['charset'], setTitle = top)
  1990.             else:
  1991.                 subLinks = self.scrapeLinks(info['body'], info['redirected-url'], setTitle = top)
  1992.             if top:
  1993.                 self.processLinks(subLinks,0,linkNumber)
  1994.             else:
  1995.                 self.processLinks(subLinks,depth+1,linkNumber)
  1996.         if len(urlList) > 0:
  1997.             self.getHTML(urlList, depth, linkNumber)
  1998.  
  1999.     def checkDone(self):
  2000.         if len(self.downloads) == 0:
  2001.             self.saveCacheHistory()
  2002.             self.updating = False
  2003.             self.ufeed.signalChange()
  2004.             self.scheduleUpdateEvents(-1)
  2005.  
  2006.     def addVideoItem(self,link,dict,linkNumber):
  2007.         link = unicodify(link.strip())
  2008.         if dict.has_key('title'):
  2009.             title = dict['title']
  2010.         else:
  2011.             title = link
  2012.         for item in self.items:
  2013.             if item.getURL() == link:
  2014.                 return
  2015.         # Anywhere we call this, we need to convert the input back to unicode
  2016.         title = feedparser.sanitizeHTML (title, "utf-8").decode('utf-8')
  2017.         if dict.has_key('thumbnail') > 0:
  2018.             i=Item(FeedParserDict({'title':title,'enclosures':[FeedParserDict({'url':link,'thumbnail':FeedParserDict({'url':dict['thumbnail']})})]}),linkNumber = linkNumber, feed_id=self.ufeed.id)
  2019.         else:
  2020.             i=Item(FeedParserDict({'title':title,'enclosures':[FeedParserDict({'url':link})]}),linkNumber = linkNumber, feed_id=self.ufeed.id)
  2021.         if self.ufeed.searchTerm is not None and not filters.matchingItems(i, self.ufeed.searchTerm):
  2022.             i.remove()
  2023.             return
  2024.  
  2025.     #FIXME: compound names for titles at each depth??
  2026.     def processLinks(self,links, depth = 0,linkNumber = 0):
  2027.         maxDepth = 2
  2028.         urls = links[0]
  2029.         links = links[1]
  2030.         # List of URLs that should be downloaded
  2031.         newURLs = []
  2032.         
  2033.         if depth<maxDepth:
  2034.             for link in urls:
  2035.                 if depth == 0:
  2036.                     linkNumber += 1
  2037.                 #print "Processing %s (%d)" % (link,linkNumber)
  2038.  
  2039.                 # FIXME: Using file extensions totally breaks the
  2040.                 # standard and won't work with Broadcast Machine or
  2041.                 # Blog Torrent. However, it's also a hell of a lot
  2042.                 # faster than checking the mime type for every single
  2043.                 # file, so for now, we're being bad boys. Uncomment
  2044.                 # the elif to make this use mime types for HTTP GET URLs
  2045.  
  2046.                 mimetype = filetypes.guessMimeType(link)
  2047.                 if mimetype is None:
  2048.                     mimetype = 'text/html'
  2049.  
  2050.                 #This is text of some sort: HTML, XML, etc.
  2051.                 if ((mimetype.startswith('text/html') or
  2052.                      mimetype.startswith('application/xhtml+xml') or 
  2053.                      mimetype.startswith('text/xml')  or
  2054.                      mimetype.startswith('application/xml') or
  2055.                      mimetype.startswith('application/rss+xml') or
  2056.                      mimetype.startswith('application/podcast+xml') or
  2057.                      mimetype.startswith('application/atom+xml') or
  2058.                      mimetype.startswith('application/rdf+xml') ) and
  2059.                     depth < maxDepth -1):
  2060.                     newURLs.append(link)
  2061.  
  2062.                 #This is a video
  2063.                 elif (mimetype.startswith('video/') or 
  2064.                       mimetype.startswith('audio/') or
  2065.                       mimetype == "application/ogg" or
  2066.                       mimetype == "application/x-annodex" or
  2067.                       mimetype == "application/x-bittorrent"):
  2068.                     self.addVideoItem(link, links[link],linkNumber)
  2069.             if len(newURLs) > 0:
  2070.                 self.getHTML(newURLs, depth, linkNumber)
  2071.  
  2072.     def onRemove(self):
  2073.         for download in self.downloads:
  2074.             logging.info ("cancling download: %s", download.url)
  2075.             download.cancel()
  2076.         self.downloads = set()
  2077.  
  2078.     #FIXME: go through and add error handling
  2079.     def update(self):
  2080.         self.ufeed.confirmDBThread()
  2081.         if not self.ufeed.idExists():
  2082.             return
  2083.         if self.updating:
  2084.             return
  2085.         else:
  2086.             self.updating = True
  2087.             self.ufeed.signalChange(needsSave=False)
  2088.  
  2089.         if not self.initialHTML is None:
  2090.             html = self.initialHTML
  2091.             self.initialHTML = None
  2092.             redirURL=self.url
  2093.             status = 200
  2094.             charset = self.initialCharset
  2095.             self.initialCharset = None
  2096.             subLinks = self.scrapeLinks(html, redirURL, charset=charset, setTitle = True)
  2097.             self.processLinks(subLinks,0,0)
  2098.             self.checkDone()
  2099.         else:
  2100.             self.getHTML([self.url], top = True)
  2101.  
  2102.     def scrapeLinks(self,html,baseurl,setTitle = False,charset = None):
  2103.         try:
  2104.             if not charset is None:
  2105.                 html = fixHTMLHeader(html,charset)
  2106.             xmldata = html
  2107.             parser = xml.sax.make_parser()
  2108.             parser.setFeature(xml.sax.handler.feature_namespaces, 1)
  2109.             try: parser.setFeature(xml.sax.handler.feature_external_ges, 0)
  2110.             except: pass
  2111.             if charset is not None:
  2112.                 handler = RSSLinkGrabber(baseurl,charset)
  2113.             else:
  2114.                 handler = RSSLinkGrabber(baseurl)
  2115.             parser.setContentHandler(handler)
  2116.             try:
  2117.                 parser.parse(StringIO(xmldata))
  2118.             except IOError, e:
  2119.                 pass
  2120.             except AttributeError:
  2121.                 # bug in the python standard library causes this to be raised
  2122.                 # sometimes.  See #3201.
  2123.                 pass
  2124.             links = handler.links
  2125.             linkDict = {}
  2126.             for link in links:
  2127.                 if link[0].startswith('http://') or link[0].startswith('https://'):
  2128.                     if not linkDict.has_key(toUni(link[0],charset)):
  2129.                         linkDict[toUni(link[0],charset)] = {}
  2130.                     if not link[1] is None:
  2131.                         linkDict[toUni(link[0],charset)]['title'] = toUni(link[1],charset).strip()
  2132.                     if not link[2] is None:
  2133.                         linkDict[toUni(link[0],charset)]['thumbnail'] = toUni(link[2],charset)
  2134.             if setTitle and not handler.title is None:
  2135.                 self.ufeed.confirmDBThread()
  2136.                 try:
  2137.                     self.title = toUni(handler.title,charset)
  2138.                 finally:
  2139.                     self.ufeed.signalChange()
  2140.             return ([x[0] for x in links if x[0].startswith('http://') or x[0].startswith('https://')], linkDict)
  2141.         except (xml.sax.SAXException, ValueError, IOError, xml.sax.SAXNotRecognizedException):
  2142.             (links, linkDict) = self.scrapeHTMLLinks(html,baseurl,setTitle=setTitle, charset=charset)
  2143.             return (links, linkDict)
  2144.  
  2145.     ##
  2146.     # Given a string containing an HTML file, return a dictionary of
  2147.     # links to titles and thumbnails
  2148.     def scrapeHTMLLinks(self,html, baseurl,setTitle=False, charset = None):
  2149.         lg = HTMLLinkGrabber()
  2150.         links = lg.getLinks(html, baseurl)
  2151.         if setTitle and not lg.title is None:
  2152.             self.ufeed.confirmDBThread()
  2153.             try:
  2154.                 self.title = toUni(lg.title, charset)
  2155.             finally:
  2156.                 self.ufeed.signalChange()
  2157.             
  2158.         linkDict = {}
  2159.         for link in links:
  2160.             if link[0].startswith('http://') or link[0].startswith('https://'):
  2161.                 if not linkDict.has_key(toUni(link[0],charset)):
  2162.                     linkDict[toUni(link[0],charset)] = {}
  2163.                 if not link[1] is None:
  2164.                     linkDict[toUni(link[0],charset)]['title'] = toUni(link[1],charset).strip()
  2165.                 if not link[2] is None:
  2166.                     linkDict[toUni(link[0],charset)]['thumbnail'] = toUni(link[2],charset)
  2167.         return ([x[0] for x in links if x[0].startswith('http://') or x[0].startswith('https://')],linkDict)
  2168.         
  2169.     ##
  2170.     # Called by pickle during deserialization
  2171.     def onRestore(self):
  2172.         FeedImpl.onRestore(self)
  2173.         #self.itemlist = defaultDatabase.filter(lambda x:isinstance(x,Item) and x.feed is self)
  2174.  
  2175.         #FIXME: the update dies if all of the items aren't restored, so we 
  2176.         # wait a little while before we start the update
  2177.         self.downloads = set()
  2178.         self.tempHistory = {}
  2179.         self.scheduleUpdateEvents(.1)
  2180.  
  2181. class DirectoryWatchFeedImpl(FeedImpl):
  2182.     def __init__(self,ufeed, directory, visible = True):
  2183.         self.dir = directory
  2184.         self.firstUpdate = True
  2185.         if directory is not None:
  2186.             url = u"dtv:directoryfeed:%s" % (makeURLSafe (directory),)
  2187.         else:
  2188.             url = u"dtv:directoryfeed"
  2189.         title = directory
  2190.         if title[-1] == '/':
  2191.             title = title[:-1]
  2192.         title = filenameToUnicode(os.path.basename(title)) + "/"
  2193.         FeedImpl.__init__(self,url = url,ufeed=ufeed,title = title,visible = visible)
  2194.  
  2195.         self.setUpdateFrequency(5)
  2196.         self.scheduleUpdateEvents(0)
  2197.  
  2198.     ##
  2199.     # Directory Items shouldn't automatically expire
  2200.     def expireItems(self):
  2201.         pass
  2202.  
  2203.     def setUpdateFrequency(self, frequency):
  2204.         newFreq = frequency*60
  2205.         if newFreq != self.updateFreq:
  2206.             self.updateFreq = newFreq
  2207.             self.scheduleUpdateEvents(-1)
  2208.  
  2209.     def setVisible(self, visible):
  2210.         if self.visible == visible:
  2211.             return
  2212.         self.visible = visible
  2213.         self.signalChange()
  2214.  
  2215.     def update(self):
  2216.         def isBasenameHidden(filename):
  2217.             if filename[-1] == os.sep:
  2218.                 filename = filename[:-1]
  2219.             return os.path.basename(filename)[0] == FilenameType('.')
  2220.         self.ufeed.confirmDBThread()
  2221.  
  2222.         # Files known about by real feeds (other than other directory
  2223.         # watch feeds)
  2224.         knownFiles = set()
  2225.         for item in views.toplevelItems:
  2226.             if not item.getFeed().getURL().startswith("dtv:directoryfeed"):
  2227.                 knownFiles.add(os.path.normcase(item.getFilename()))
  2228.  
  2229.         # Remove items that are in feeds, but we have in our list
  2230.         for item in self.items:
  2231.             if item.getFilename() in knownFiles:
  2232.                 item.remove()
  2233.  
  2234.         # Now that we've checked for items that need to be removed, we
  2235.         # add our items to knownFiles so that they don't get added
  2236.         # multiple times to this feed.
  2237.         for x in self.items:
  2238.             knownFiles.add(os.path.normcase (x.getFilename()))
  2239.  
  2240.         #Adds any files we don't know about
  2241.         #Files on the filesystem
  2242.         if os.path.isdir(self.dir):
  2243.             all_files = []
  2244.             files, dirs = miro_listdir(self.dir)
  2245.             for file in files:
  2246.                 all_files.append(file)
  2247.             for dir in dirs:
  2248.                 subfiles, subdirs = miro_listdir(dir)
  2249.                 for subfile in subfiles:
  2250.                     all_files.append(subfile)
  2251.             for file in all_files:
  2252.                 if file not in knownFiles and filetypes.isVideoFilename(platformutils.filenameToUnicode(file)):
  2253.                     FileItem(file, feed_id=self.ufeed.id)
  2254.  
  2255.         for item in self.items:
  2256.             if not os.path.isfile(item.getFilename()):
  2257.                 item.remove()
  2258.         if self.firstUpdate:
  2259.             for item in self.items:
  2260.                 item.markItemSeen()
  2261.             self.firstUpdate = False
  2262.  
  2263.         self.scheduleUpdateEvents(-1)
  2264.  
  2265.     def onRestore(self):
  2266.         FeedImpl.onRestore(self)
  2267.         #FIXME: the update dies if all of the items aren't restored, so we 
  2268.         # wait a little while before we start the update
  2269.         self.scheduleUpdateEvents(.1)
  2270.  
  2271. ##
  2272. # A feed of all of the Movies we find in the movie folder that don't
  2273. # belong to a "real" feed.  If the user changes her movies folder, this feed
  2274. # will continue to remember movies in the old folder.
  2275. #
  2276. class DirectoryFeedImpl(FeedImpl):
  2277.     def __init__(self,ufeed):
  2278.         FeedImpl.__init__(self,url = u"dtv:directoryfeed",ufeed=ufeed,title = u"Feedless Videos",visible = False)
  2279.  
  2280.         self.setUpdateFrequency(5)
  2281.         self.scheduleUpdateEvents(0)
  2282.  
  2283.     ##
  2284.     # Directory Items shouldn't automatically expire
  2285.     def expireItems(self):
  2286.         pass
  2287.  
  2288.     def setUpdateFrequency(self, frequency):
  2289.         newFreq = frequency*60
  2290.         if newFreq != self.updateFreq:
  2291.                 self.updateFreq = newFreq
  2292.                 self.scheduleUpdateEvents(-1)
  2293.  
  2294.     def update(self):
  2295.         self.ufeed.confirmDBThread()
  2296.         moviesDir = config.get(prefs.MOVIES_DIRECTORY)
  2297.         # Files known about by real feeds
  2298.         knownFiles = set()
  2299.         for item in views.toplevelItems:
  2300.             if item.feed_id is not self.ufeed.id:
  2301.                 knownFiles.add(os.path.normcase(item.getFilename()))
  2302.             if item.isContainerItem:
  2303.                 item.findNewChildren()
  2304.  
  2305.         knownFiles.add(os.path.normcase(os.path.join(moviesDir, "Incomplete Downloads")))
  2306.  
  2307.         # Remove items that are in feeds, but we have in our list
  2308.         for item in self.items:
  2309.             if item.getFilename() in knownFiles:
  2310.                 item.remove()
  2311.  
  2312.         # Now that we've checked for items that need to be removed, we
  2313.         # add our items to knownFiles so that they don't get added
  2314.         # multiple times to this feed.
  2315.         for x in self.items:
  2316.             knownFiles.add(os.path.normcase (x.getFilename()))
  2317.  
  2318.         #Adds any files we don't know about
  2319.         #Files on the filesystem
  2320.         if os.path.isdir(moviesDir):
  2321.             files, dirs = miro_listdir(moviesDir)
  2322.             for file in files:
  2323.                 if not file in knownFiles:
  2324.                     FileItem(file, feed_id=self.ufeed.id)
  2325.             for dir in dirs:
  2326.                 if dir in knownFiles:
  2327.                     continue
  2328.                 found = 0
  2329.                 not_found = []
  2330.                 subfiles, subdirs = miro_listdir(dir)
  2331.                 for subfile in subfiles:
  2332.                     if subfile in knownFiles:
  2333.                         found = found + 1
  2334.                     else:
  2335.                         not_found.append(subfile)
  2336.                 for subdir in subdirs:
  2337.                     if subdir in knownFiles:
  2338.                         found = found + 1
  2339.                 # If every subfile or subdirectory is
  2340.                 # already in the database (including
  2341.                 # the case where the directory is
  2342.                 # empty) do nothing.
  2343.                 if len(not_found) > 0:
  2344.                     # If there were any files found,
  2345.                     # this is probably a channel
  2346.                     # directory that someone added
  2347.                     # some thing to.  There are few
  2348.                     # other cases where a directory
  2349.                     # would have some things shown.
  2350.                     if found != 0:
  2351.                         for subfile in not_found:
  2352.                             FileItem(subfile, feed_id=self.ufeed.id)
  2353.                     # But if not, it's probably a
  2354.                     # directory added wholesale.
  2355.                     else:
  2356.                         FileItem(dir, feed_id=self.ufeed.id)
  2357.  
  2358.         for item in self.items:
  2359.             if not os.path.exists(item.getFilename()):
  2360.                 item.remove()
  2361.  
  2362.         self.scheduleUpdateEvents(-1)
  2363.  
  2364.     def onRestore(self):
  2365.         FeedImpl.onRestore(self)
  2366.         #FIXME: the update dies if all of the items aren't restored, so we 
  2367.         # wait a little while before we start the update
  2368.         self.scheduleUpdateEvents(.1)
  2369.  
  2370. ##
  2371. # Search and Search Results feeds
  2372.  
  2373. class SearchFeedImpl (RSSMultiFeedImpl):
  2374.     
  2375.     def __init__(self, ufeed):
  2376.         RSSMultiFeedImpl.__init__(self, url=u'', ufeed=ufeed, title=u'dtv:search', visible=False)
  2377.         self.initialUpdate = True
  2378.         self.setUpdateFrequency(-1)
  2379.         self.searching = False
  2380.         self.lastEngine = u'all'
  2381.         self.lastQuery = u''
  2382.         self.ufeed.autoDownloadable = False
  2383.         self.ufeed.signalChange()
  2384.  
  2385.     @returnsUnicode
  2386.     def quoteLastQuery(self):
  2387.         return escape(self.lastQuery)
  2388.  
  2389.     @returnsUnicode
  2390.     def getURL(self):
  2391.         return u'dtv:search'
  2392.  
  2393.     @returnsUnicode
  2394.     def getTitle(self):
  2395.         return _(u'Search')
  2396.  
  2397.     @returnsUnicode
  2398.     def getStatus(self):
  2399.         status = u'idle-empty'
  2400.         if self.searching:
  2401.             status =  u'searching'
  2402.         elif len(self.items) > 0:
  2403.             status =  u'idle-with-results'
  2404.         elif self.url:
  2405.             status = u'idle-no-results'
  2406.         return status
  2407.  
  2408.     def reset(self, url=u'', searchState=False):
  2409.         self.ufeed.confirmDBThread()
  2410.         try:
  2411.             self.initialUpdate = True
  2412.             for item in self.items:
  2413.                 item.remove()
  2414.             self.url = url
  2415.             self.splitURLs()
  2416.             self.searching = searchState
  2417.             self.etag = {}
  2418.             self.modified = {}
  2419.             self.title = self.url
  2420.             self.ufeed.iconCache.reset()
  2421.             self.thumbURL = defaultFeedIconURL()
  2422.             self.ufeed.iconCache.requestUpdate(is_vital=True)
  2423.         finally:
  2424.             self.ufeed.signalChange()
  2425.     
  2426.     def preserveDownloads(self, downloadsFeed):
  2427.         self.ufeed.confirmDBThread()
  2428.         for item in self.items:
  2429.             if item.getState() not in ('new', 'not-downloaded'):
  2430.                 item.setFeed(downloadsFeed.id)
  2431.  
  2432.     def lookup(self, engine, query):
  2433.         checkU(engine)
  2434.         checkU(query)
  2435.         url = searchengines.getRequestURL(engine, query)
  2436.         self.reset(url, True)
  2437.         self.lastQuery = query
  2438.         self.lastEngine = engine
  2439.         self.update()
  2440.         self.ufeed.signalChange()
  2441.  
  2442.     def _handleNewEntry(self, entry, channelTitle):
  2443.         """Handle getting a new entry from a feed."""
  2444.         videoEnc = getFirstVideoEnclosure(entry)
  2445.         if videoEnc is not None:
  2446.             url = videoEnc.get('url')
  2447.             if url is not None:
  2448.                 dl = downloader.getExistingDownloaderByURL(url)
  2449.                 if dl is not None:
  2450.                     for item in dl.itemList:
  2451.                         if item.getFeedURL() == 'dtv:searchDownloads' and item.getURL() == url:
  2452.                             try:
  2453.                                 if entry["id"] == item.getRSSID():
  2454.                                     item.setFeed(self.ufeed.id)
  2455.                                     if not _entry_equal(entry, item.getRSSEntry()):
  2456.                                         self._handleNewEntryForItem(item, entry, channelTitle)
  2457.                                     return
  2458.                             except KeyError:
  2459.                                 pass
  2460.                             title = entry.get("title")
  2461.                             oldtitle = item.entry.get("title")
  2462.                             if title == oldtitle:
  2463.                                 item.setFeed(self.ufeed.id)
  2464.                                 if not _entry_equal(entry, item.getRSSEntry()):
  2465.                                     self._handleNewEntryForItem(item, entry, channelTitle)
  2466.                                 return
  2467.         RSSMultiFeedImpl._handleNewEntry(self, entry, channelTitle)
  2468.  
  2469.     def updateFinished(self):
  2470.         self.searching = False
  2471.         RSSMultiFeedImpl.updateFinished(self)
  2472.  
  2473.     def update(self):
  2474.         if self.url is not None and self.url != u'':
  2475.             RSSMultiFeedImpl.update(self)
  2476.  
  2477. class SearchDownloadsFeedImpl(FeedImpl):
  2478.     def __init__(self, ufeed):
  2479.         FeedImpl.__init__(self, url=u'dtv:searchDownloads', ufeed=ufeed, 
  2480.                 title=None, visible=False)
  2481.         self.setUpdateFrequency(-1)
  2482.  
  2483.     @returnsUnicode
  2484.     def getTitle(self):
  2485.         return _(u'Search')
  2486.  
  2487. class ManualFeedImpl(FeedImpl):
  2488.     """Downloaded Videos/Torrents that have been added using by the
  2489.     user opening them with democracy.
  2490.     """
  2491.     def __init__(self, ufeed):
  2492.         FeedImpl.__init__(self, url=u'dtv:manualFeed', ufeed=ufeed, 
  2493.                 title=None, visible=False)
  2494.         self.ufeed.expire = u'never'
  2495.         self.setUpdateFrequency(-1)
  2496.  
  2497.     @returnsUnicode
  2498.     def getTitle(self):
  2499.         return _(u'Local Files')
  2500.  
  2501. class SingleFeedImpl(FeedImpl):
  2502.     """Single Video that is playing that has been added by the user
  2503.     opening them with democracy.
  2504.     """
  2505.     def __init__(self, ufeed):
  2506.         FeedImpl.__init__(self, url=u'dtv:singleFeed', ufeed=ufeed, 
  2507.                 title=None, visible=False)
  2508.         self.ufeed.expire = u'never'
  2509.         self.setUpdateFrequency(-1)
  2510.  
  2511.     @returnsUnicode
  2512.     def getTitle(self):
  2513.         return _(u'Playing File')
  2514.  
  2515. ##
  2516. # Parse HTML document and grab all of the links and their title
  2517. # FIXME: Grab link title from ALT tags in images
  2518. # FIXME: Grab document title from TITLE tags
  2519. class HTMLLinkGrabber(HTMLParser):
  2520.     linkPattern = re.compile("<(a|embed)\s[^>]*(href|src)\s*=\s*\"([^\"]*)\"[^>]*>(.*?)</a(.*)", re.S)
  2521.     imgPattern = re.compile(".*<img\s.*?src\s*=\s*\"(.*?)\".*?>", re.S)
  2522.     tagPattern = re.compile("<.*?>")
  2523.     def getLinks(self,data, baseurl):
  2524.         self.links = []
  2525.         self.lastLink = None
  2526.         self.inLink = False
  2527.         self.inObject = False
  2528.         self.baseurl = baseurl
  2529.         self.inTitle = False
  2530.         self.title = None
  2531.         self.thumbnailUrl = None
  2532.  
  2533.         match = HTMLLinkGrabber.linkPattern.search(data)
  2534.         while match:
  2535.             try:
  2536.                 linkURL = match.group(3).encode('ascii')
  2537.             except UnicodeError:
  2538.                 linkURL = match.group(3)
  2539.                 i = len (linkURL) - 1
  2540.                 while (i >= 0):
  2541.                     if 127 < ord(linkURL[i]) <= 255:
  2542.                         linkURL = linkURL[:i] + "%%%02x" % (ord(linkURL[i])) + linkURL[i+1:]
  2543.                     i = i - 1
  2544.  
  2545.             link = urljoin(baseurl, linkURL)
  2546.             desc = match.group(4)
  2547.             imgMatch = HTMLLinkGrabber.imgPattern.match(desc)
  2548.             if imgMatch:
  2549.                 try:
  2550.                     thumb = urljoin(baseurl, imgMatch.group(1).encode('ascii'))
  2551.                 except UnicodeError:
  2552.                     thumb = None
  2553.             else:
  2554.                 thumb = None
  2555.             desc =  HTMLLinkGrabber.tagPattern.sub(' ',desc)
  2556.             self.links.append((link, desc, thumb))
  2557.             match = HTMLLinkGrabber.linkPattern.search(match.group(5))
  2558.         return self.links
  2559.  
  2560. class RSSLinkGrabber(xml.sax.handler.ContentHandler, xml.sax.handler.ErrorHandler):
  2561.     def __init__(self,baseurl,charset=None):
  2562.         self.baseurl = baseurl
  2563.         self.charset = charset
  2564.     def startDocument(self):
  2565.         #print "Got start document"
  2566.         self.enclosureCount = 0
  2567.         self.itemCount = 0
  2568.         self.links = []
  2569.         self.inLink = False
  2570.         self.inDescription = False
  2571.         self.inTitle = False
  2572.         self.inItem = False
  2573.         self.descHTML = ''
  2574.         self.theLink = ''
  2575.         self.title = None
  2576.         self.firstTag = True
  2577.         self.errors = 0
  2578.         self.fatalErrors = 0
  2579.  
  2580.     def startElementNS(self, name, qname, attrs):
  2581.         uri = name[0]
  2582.         tag = name[1]
  2583.         if self.firstTag:
  2584.             self.firstTag = False
  2585.             if tag not in ['rss','feed']:
  2586.                 raise xml.sax.SAXNotRecognizedException, "Not an RSS file"
  2587.         if tag.lower() == 'enclosure' or tag.lower() == 'content':
  2588.             self.enclosureCount += 1
  2589.         elif tag.lower() == 'link':
  2590.             self.inLink = True
  2591.             self.theLink = ''
  2592.         elif tag.lower() == 'description':
  2593.             self.inDescription = True
  2594.             self.descHTML = ''
  2595.         elif tag.lower() == 'item':
  2596.             self.itemCount += 1
  2597.             self.inItem = True
  2598.         elif tag.lower() == 'title' and not self.inItem:
  2599.             self.inTitle = True
  2600.  
  2601.     def endElementNS(self, name, qname):
  2602.         uri = name[0]
  2603.         tag = name[1]
  2604.         if tag.lower() == 'description':
  2605.             lg = HTMLLinkGrabber()
  2606.             try:
  2607.                 html = xhtmlify(unescape(self.descHTML),addTopTags=True)
  2608.                 if not self.charset is None:
  2609.                     html = fixHTMLHeader(html,self.charset)
  2610.                 self.links[:0] = lg.getLinks(html,self.baseurl)
  2611.             except HTMLParseError: # Don't bother with bad HTML
  2612.                 logging.info ("bad HTML in description for %s", self.baseurl)
  2613.             self.inDescription = False
  2614.         elif tag.lower() == 'link':
  2615.             self.links.append((self.theLink,None,None))
  2616.             self.inLink = False
  2617.         elif tag.lower() == 'item':
  2618.             self.inItem == False
  2619.         elif tag.lower() == 'title' and not self.inItem:
  2620.             self.inTitle = False
  2621.  
  2622.     def characters(self, data):
  2623.         if self.inDescription:
  2624.             self.descHTML += data
  2625.         elif self.inLink:
  2626.             self.theLink += data
  2627.         elif self.inTitle:
  2628.             if self.title is None:
  2629.                 self.title = data
  2630.             else:
  2631.                 self.title += data
  2632.  
  2633.     def error(self, exception):
  2634.         self.errors += 1
  2635.  
  2636.     def fatalError(self, exception):
  2637.         self.fatalErrors += 1
  2638.  
  2639. # Grabs the feed link from the given webpage
  2640. class HTMLFeedURLParser(HTMLParser):
  2641.     def getLink(self,baseurl,data):
  2642.         self.baseurl = baseurl
  2643.         self.link = None
  2644.         try:
  2645.             self.feed(data)
  2646.         except HTMLParseError:
  2647.             logging.info ("error parsing %s", baseurl)
  2648.         try:
  2649.             self.close()
  2650.         except HTMLParseError:
  2651.             logging.info ("error closing %s", baseurl)
  2652.         return self.link
  2653.  
  2654.     def handle_starttag(self, tag, attrs):
  2655.         attrdict = {}
  2656.         for (key, value) in attrs:
  2657.             attrdict[key.lower()] = value
  2658.         if (tag.lower() == 'link' and attrdict.has_key('rel') and 
  2659.             attrdict.has_key('type') and attrdict.has_key('href') and
  2660.             attrdict['rel'].lower() == 'alternate' and 
  2661.             attrdict['type'].lower() in ['application/rss+xml',
  2662.                                          'application/podcast+xml',
  2663.                                          'application/rdf+xml',
  2664.                                          'application/atom+xml',
  2665.                                          'text/xml',
  2666.                                          'application/xml']):
  2667.             self.link = urljoin(self.baseurl,attrdict['href'])
  2668.  
  2669. def expireItems():
  2670.     try:
  2671.         for feed in views.feeds:
  2672.             feed.expireItems()
  2673.     finally:
  2674.         eventloop.addTimeout(300, expireItems, "Expire Items")
  2675.  
  2676. def getFeedByURL(url):
  2677.     return views.feeds.getItemWithIndex(indexes.feedsByURL, url)
  2678.